Files
buzz/docs/nips
Will Pfleger 187df22252 docs(nip-fi): adopt deny-until-TTL and extend enforcement to HTTP ingress (#7254)
Spec revision following two decisions: Option B admin deny (2026-09-02)
and the HTTP ingress ruling (2026-09-02). Revises `docs/nips/NIP-FI.md`
only. Follows #7214 (merged spec v2).

## What changed

### Admin disconnect: session-only → deny-until-TTL

The disconnect operation proceeds in two steps, in order:

1. Insert a memory-resident deny entry keyed by `(iss, target_pubkey)`
with absolute expiry `until` — atomically combined with the `(iss, jti)`
replay-identity reservation as one all-or-nothing mutation. If the deny
set is at capacity (per-issuer bound), the relay rejects `503`; neither
the jti nor the deny entry is recorded, and the caller may safely retry
the same signed command.
2. Close all live WebSocket connections for the target pubkey,
synchronously.

The single atomic admission mutation (jti reservation + deny-entry
insertion) lives inside `VerifyCommandJwt` step 7, after all pure
authorization checks. The endpoint only closes sessions on success. This
ordering ensures a capacity failure leaves no state behind and makes the
retry-safe 503 contract implementable.

The deny set is RAM-cache only — no durable storage, no schema changes.
The same operational posture as the JWKS snapshot. A relay restart MAY
forget active entries; the issuer SHOULD re-push still-active deny
entries on observed restart (same publish/cache pattern as JWKS). If the
issuer stops issuing assertions and re-push completes before any
expired-entry reconnection attempt, residual exposure after restart is
bounded by `max(0, min(exp, iat + maximum_assertion_age) - now)`. If the
issuer continues issuing or re-push does not complete in time, that
formula does not apply and access may continue beyond it.

**`until` claim:** Required on the disconnect command JWT. Because an
assertion accepted at the future-skew boundary (`iat <= now + skew`)
remains valid until `iat + maximum_assertion_age`, the latest possible
authority deadline is `now + skew + maximum_assertion_age`. The relay
enforces `until <= now + skew + maximum_assertion_age`. A value above
this ceiling rejects `400`; a past `until` still closes live sessions —
absent an active same-key entry it creates no future denial, while an
active entry remains unchanged under the merge rule.

**Capacity and eviction (per-issuer):** The relay MUST bound the deny
set size **per issuer**. Capacity exhaustion under one issuer MUST NOT
cause rejection of another issuer's commands; the `503` capacity check
is evaluated against the command's own issuer bound. Implementations
MUST evict only expired entries; when an issuer's partition is at
capacity and all entries are still active, the relay MUST reject the new
command `503` without removing any existing entry. There is no LRU
eviction of active denies.

**Issuer-global deny:** The deny entry applies to admission across all
communities served by the relay under that issuer. Identity-level
revocation is intentionally not community-partial.

**Cross-replica propagation:** In a deployment with multiple relay
processes, the deployment MUST propagate both the session-close and the
deny entry to every process serving admissions for the issuer's
communities. The mechanism is deployment-defined (e.g. the existing
inter-process message bus, same posture as JWKS convergence).
Propagation is asynchronous with no protocol-level completion bound. The
issuer re-push duty is the recovery path for lost propagation, exactly
as for relay restart.

**Response shape:** A successful disconnect responds `{"disconnected":
true}` regardless of how many sessions were closed. No session count is
returned; a count would aggregate activity across communities and
constitute an information leak.

**Admission procedure:** Step 5 registers the session's proven `k`
before the deny-set check (new step 6) — ensuring any connection that
straddles a concurrent disconnect is caught by one side or the other.
`FI-TRACE-DENY-SET` oracle covers the per-issuer capacity rule and the
straddling termination requirement.

### HTTP ingress enforcement

Without explicit enforcement, a protected HTTP surface (bridge, invites,
media, git) with NIP-98-only authorization allows a principal holding an
active key to mint fresh NIP-98 events indefinitely — NIP-98 proves key
possession only, not identity. Without assertion verification there is
no expiry bound; the key remains valid for as long as it is accepted.

**Pairing rule:** in enforce mode, a protected HTTP request MUST carry
both:

```
Authorization: Nostr <base64-NIP-98-event>
Nostr-Federated-Identity: Bearer <compact-JWS>
```

The NIP-98 pubkey MUST equal the assertion `nostr_pubkey` claim.
Missing, mismatched, or invalid evidence of either kind denies, fail
closed.

**Verification:** reuses `VerifyAssertion` unchanged — offline, same
JWKS, same claim requirements, same denial classes.

**Per-request:** HTTP is sessionless; every request re-verifies. No
session lifetime, no cached admission. The cumulative residual bound
applies per request.

**Deny-set applicability:** the deny-until-TTL entry introduced above is
consulted per HTTP request identically to WebSocket admission.

**Protected surface:** deployment-configured set of routes, fail-closed
default (unclassifiable routes treated as protected). No normative route
names in the spec.

`FI-TRACE-HTTP-INGRESS` oracle added. Security considerations updated
with HTTP ingress bypass analysis. NIP-98 source reference added.

### Other changes

- `authorization_denied` rejection table row updated to "active deny-set
entry for pubkey".
- Discovery: `maximum_residual_upstream_revocation_seconds` remains
`null` — the deny-until-TTL model is best-effort RAM state and provides
no unconditional finite revocation bound.
- Rejection and privacy: explicit sentence for HTTP denial path.
- Client-attached transport: opening sentence generalized to cover both
WebSocket and HTTP.

## Scope

Single file: `docs/nips/NIP-FI.md`. No code changes.

References #7214. Channel: buzz-enterprise-identity-spec-v2
(#a6fe0b1c-987a-43c5-a974-71ee36678d78).

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
2026-09-02 17:24:14 -04:00
..
…
…
…