docs(nip-fi): clarify federated identity amendments (#7803)

## Summary

Aligns the NIP-FI wire contract with the post-sync enterprise identity
decisions:

- bind each community's canonical host URI as its RFC 8707
resource-indicator audience, resolve `Host` to an immutable community
configuration before verification, and require both exact audience
binding and explicit community-to-issuer authorization
- require relays to ignore unverified claims and avoid logging or
exposing them; recommend that issuers minimize relay-visible claims
- make per-session (`sid`) revocation issuer-side while keeping the
relay's revocation primitive pubkey-scoped and community-wide, keyed
`(iss, target_pubkey)`
- scope the disconnect close scan to connections registered under the
command's issuer, including admission-in-progress, matching the
deny-entry key; terminally cancel pending connections and close the
cross-issuer session-termination gap
- clarify proactive assertion re-exchange and reconnect before expiry
without introducing in-band renewal

Originating Buzz discussion:
`buzz://message?channel=51940054-5a7a-4773-a337-8004496630fa&id=d551ee7c61882ef33843e752f63a837921e7c5df2671a88c68a84524490089cc`

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
Signed-off-by: Paul <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@buzz.block.builderlab.xyz>
Co-authored-by: Alia <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@buzz.block.builderlab.xyz>
Co-authored-by: Paul <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@buzz.block.builderlab.xyz>
This commit is contained in:
Will Pfleger
2026-09-22 17:56:45 -04:00
committed by GitHub
parent a024b83d54
commit 26ede6dfa2

View File

@@ -18,8 +18,9 @@ NIP-FI authorizes a Nostr key when two independent facts agree: a valid
issuer-qualified identity assertion that names the key, and fresh NIP-42 proof
of possession of that key. No relay-side identity state is required. The
relay verifies the assertion offline against configured per-issuer JWKS
snapshots; every identity decision beyond key verification is the assertion
issuer's responsibility.
snapshots and enforces the resolved community's audience and issuer
authorization policy; other identity decisions remain the assertion issuer's
responsibility.
This NIP defines the assertion contract, the offline verification procedure,
session lifetime policy, and an authenticated issuer→relay disconnect API.
@@ -40,6 +41,9 @@ this spec.
OIDC identity provider integration) that authenticates users and mints
assertions. The relay trusts only the issuer's assertion; it does not
contact the IdP directly.
- **community**: the tenant resolved from a connection's `Host`. A community
has one configured canonical host URI, which is its assertion audience, and
an explicit set of issuer URIs authorized to mint assertions for it.
## Assertion contract
@@ -52,7 +56,7 @@ The assertion is a compact JWS carrying the following claims.
| `iss` | string | Exact issuer URI. The relay selects an issuer policy by exact match; no normalization is applied. |
| `sub` | string | Opaque, stable, non-reassignable subject identifier for the account lifetime. Never an email address or display name. |
| `nostr_pubkey` | string | Lowercase hexadecimal encoding of exactly one 32-byte Nostr public key. Other encodings deny. |
| `aud` | string or array | Audience. MUST be present. The relay requires an exact match to the configured audience value for this issuer. |
| `aud` | string or array | Audience. MUST be present. The configured audience value for each community MUST be that community's canonical host URI, following the resource-indicator practice in RFC 8707. One community MUST map to one canonical audience value; distinct communities MUST NOT share an audience value. The relay MUST require the assertion audience to exactly match the expected audience of the community resolved from the connection's `Host`; issuer policy selection by `iss` MUST NOT broaden this community binding. |
| `iat` | NumericDate | Issuance time. |
| `exp` | NumericDate | Expiry time. MUST be finite. The deployment MUST configure a positive finite maximum TTL; the relay enforces both the token `exp` and the configured `maximum_assertion_age`. |
@@ -111,6 +115,26 @@ This is the entire identity-to-key binding. There is no relay-side binding
ledger; the assertion is the binding claim, and it is the assertion issuer's
responsibility to ensure the assertion names the correct key.
Community resolution MUST precede assertion verification. The relay MUST resolve the
connection's `Host` to exactly one community, its expected audience, and its
configured issuer allowlist; an unknown or unresolvable host, or unavailable
community configuration, MUST fail closed with `authorization_unavailable`.
After bounded decoding looks up the exact issuer policy by `iss`, the relay MUST
immediately require that issuer to be authorized for the resolved community
before touching any issuer-specific dependency, including JWKS. An untrusted
`iss` is used only to select or reject a candidate policy; it grants no
authority. Signature verification remains mandatory before acceptance.
`VerifyAssertion` MUST compare `aud` with the resolved community's expected
audience; issuer policy selection by `iss` MUST NOT broaden either binding.
### Unverified claims
Relays MUST ignore claims they do not verify. An unverified claim — including
`sid`, `jti`, or issuer-private blocks such as `enterprise_identity` — grants no
authority and MUST NOT be logged or exposed in discovery or error responses.
Issuers SHOULD minimize claims in assertions visible to relays to reduce
unnecessary disclosure.
### Policy identity
```text
@@ -118,11 +142,12 @@ AssertionPolicyId = H(canonical assertion-policy contract)
TransportContractId = H(canonical transport contract)
```
`AssertionPolicyId` covers the canonical issuer, audience, token class,
allowed algorithms, key-source contract, identity/key/claim mapping, time and
size rules, and compiled verifier behavior. JWKS key rotation changes the
snapshot, not the policy ID. `TransportContractId` covers the client-attached
field, parsing, attachment, and no-fallback semantics.
`AssertionPolicyId` covers the canonical issuer, community issuer
association, audience, token class, allowed algorithms, key-source contract,
identity/key/claim mapping, time and size rules, and compiled verifier
behavior. JWKS key rotation changes the snapshot, not the policy ID.
`TransportContractId` covers the client-attached field, parsing, attachment,
and no-fallback semantics.
## Client-attached transport
@@ -147,16 +172,32 @@ Failure never falls back to another transport.
The relay verifies assertions **offline** against configured per-issuer JWKS
snapshots. No IdP contact occurs at admission time.
### Community issuer authorization
Each configured community MUST declare an explicit allowlist of issuer URIs
that may authorize identities for that community. An issuer policy selected by
exact `iss` matching is usable only when that issuer is present in the resolved
community's allowlist. A global `IssuerRegistry` MAY hold the shared policy and
JWKS configuration, but it MUST NOT by itself authorize an issuer for every
community. Deployments MUST reject a community configuration that has no
canonical host URI, has an ambiguous Host mapping, or grants an issuer access
without an explicit community association.
The community allowlist and its canonical audience are deployment policy, not
claims supplied by the requester. The relay MUST resolve them before assertion
verification and MUST fail closed with `authorization_unavailable` when the
community configuration is missing or unavailable.
### Multi-issuer registry
The relay maintains one [`IssuerRegistry`](../../crates/buzz-auth/src/nip_fi/config.rs):
a map from exact `iss` strings to issuer policies. The `iss` carried in the
signed token selects exactly one policy; unknown issuers deny. A
single-issuer deployment is a registry of length one. [FI-TRACE-CROSS-DOMAIN-COLLISION]
The global [`IssuerRegistry`](../../crates/buzz-auth/src/nip_fi/config.rs) remains
shared for issuer policy and JWKS lookup; community authorization is a separate
deployment mapping from canonical host URI to
`(expected_aud, authorized_issuers)`. `VerifyAssertion` consumes the
Host-resolved mapping, so a globally known issuer is not implicitly trusted by
every community.
The existing `FederatedAssertionVerifier<S>` and `ProductionJwksSource<F>`
(merged in PR 3 / `70895b355`) implement the verification procedure described
here. The `nostr_pubkey` claim is unconditionally required — absence rejects
The `nostr_pubkey` claim is unconditionally required — absence rejects
regardless of issuer policy (NIP-FI v2, PR #7221).
### JWKS snapshot
@@ -183,10 +224,15 @@ any attacker-controlled `kid` lookup.
### Verification procedure
```text
VerifyAssertion(token, D, R_t):
// 1. Select issuer policy
VerifyAssertion(token, community, D, R_t):
// `community` is the immutable result of Host resolution by the caller;
// it contains `expected_aud` and the authorized issuer URI allowlist.
AssertCommunityConfig(community) or DENY(authorization_unavailable)
// 1. Select issuer policy and authorize it for this community
(header, claims) := BoundedJwsDecode(token) or DENY(evidence_rejected)
policy := IssuerRegistry[claims.iss] or DENY(evidence_rejected)
AssertIssuerAuthorized(policy.iss, community.authorized_issuers) or DENY(evidence_rejected)
// 2. Validate token class, typ, and algorithm
ValidateTokenClass(policy, header) or DENY(evidence_rejected)
@@ -197,9 +243,9 @@ VerifyAssertion(token, D, R_t):
key := snapshot.find(header.kid) or DENY(evidence_rejected)
VerifySignature(token, key) or DENY(evidence_rejected)
// 4. Validate claims
// 4. Validate remaining claims against the resolved community and issuer policy
AssertExactIss(claims.iss, policy.iss) or DENY(evidence_rejected)
AssertAudienceMatch(claims.aud, policy.aud) or DENY(evidence_rejected)
AssertAudienceMatch(claims.aud, community.expected_aud) or DENY(evidence_rejected)
AssertTimeBounds(claims, policy) or DENY(evidence_rejected) // [FI-TRACE-ASSERTION-VALIDATION]
k_claimed := ParseHexKey(claims.nostr_pubkey) or DENY(evidence_rejected)
@@ -208,30 +254,39 @@ VerifyAssertion(token, D, R_t):
```
The verifier is **fail-closed**: any unreadable, missing, ambiguous, or
expired input denies. A missing JWKS snapshot denies with
`authorization_unavailable`; all other failures deny with `evidence_rejected`.
expired input denies. A missing or unavailable JWKS snapshot or community
configuration denies with `authorization_unavailable`; all other failures deny
with `evidence_rejected`.
[FI-TRACE-DEPENDENCY-FAIL-CLOSED]
### Admission at connection
On WebSocket upgrade:
1. Extract `Nostr-Federated-Identity` header; missing or malformed → deny
`missing_evidence` or `evidence_rejected`.
2. Call `VerifyAssertion`; any error → deny per the rejection table.
3. Complete NIP-42 handshake; validate AUTH event, extract `k`.
4. Assert `verified.asserted_key == k`; mismatch → deny `authorization_denied`.
1. Resolve the connection's `Host` to `community`, including its canonical
`expected_aud` and configured issuer allowlist; unknown, ambiguous, or
unavailable community resolution → deny `authorization_unavailable`.
2. Extract the compact JWS `token` from `Nostr-Federated-Identity`; missing or
malformed → deny `missing_evidence` or `evidence_rejected`.
3. Call `VerifyAssertion(token, community, D, R_t)`; any error → deny per the
rejection table.
4. Complete NIP-42 handshake; validate AUTH event, extract `k`.
5. Assert `verified.asserted_key == k`; mismatch → deny `authorization_denied`.
[FI-TRACE-ASSERTION-KEY-MISMATCH]
5. Register the session's proven `k` in the relay's session table, making it
visible to the disconnect close scan. Registration MUST occur before the
deny-set check in step 6. This ordering ensures any connection that straddles
a concurrent disconnect is caught by one side or the other: either the close
scan sees the registered session, or the deny-set check (step 6) sees the
inserted entry.
6. Check deny set for `(iss, k)`; active entry (`now < until`) → deny
6. Register the session's proven `k` and admitting issuer `iss` in the
relay's session table, making it visible to the disconnect close scan.
Registration MUST occur before the deny-set check in step 7. This ordering
ensures any connection that straddles a concurrent disconnect is caught by
one side or the other: either the close scan sees the registered session, or
the deny-set check (step 7) sees the inserted entry. Both checks use the
same `(iss, k)` key. A concurrent disconnect close scan MAY cancel this
registered connection before step 8; cancellation is terminal, so a
cancelled connection MUST NOT be admitted.
7. Check deny set for `(iss, k)`; active entry (`now < until`) → deny
`authorization_denied`. [FI-TRACE-DENY-SET]
7. Admit the connection. The session's authority deadline is the minimum of all
`authority_deadlines`; see Session policy.
8. Admit the connection only if it remains live and uncancelled. The session's
authority deadline is the minimum of all `authority_deadlines`; see Session
policy.
## Session policy
@@ -271,6 +326,9 @@ detail; the relay never sees anything other than a new upgrade request.
A client whose session expired due to normal TTL expiry may reconnect
immediately provided the issuer can supply a fresh assertion. Session expiry
does not imply key revocation or identity loss; that is the issuer's domain.
Clients SHOULD proactively obtain a fresh assertion before expiry and reconnect
with it. Nothing in this section forbids early re-exchange; renewal remains
out of band rather than an in-band session operation.
## Admin disconnect API
@@ -292,8 +350,11 @@ A disconnect call causes the relay to:
denied `authorization_denied` until `now >= until`. If the deny set is at
capacity and a new entry cannot be inserted, the relay MUST reject the command
`503`; no sessions are closed and no replay state is consumed.
2. Close all live WebSocket connections whose proven `k` equals the target
pubkey, synchronously.
2. Close all live WebSocket connections registered under the command's issuer
whose proven `k` equals the target pubkey, whether or not admission has
completed, synchronously. Disconnect cancellation is terminal: a cancelled
in-progress connection MUST NOT be subsequently admitted. Connections
registered under another issuer are not affected.
The deny set is held **in relay memory only** — no durable storage, no schema
changes. A relay restart MAY forget active deny entries. If the issuer stops
@@ -322,7 +383,7 @@ is immediately expired); this is not an error.
The deny entry is keyed `(iss, target_pubkey)` and applies to admission
across **all communities** served by the relay under that issuer.
Identity-level revocation is intentionally not community-partial: a key revoked
Key-level revocation is intentionally not community-partial: a key revoked
by its issuer loses access in every community that issuer governs.
In a deployment with multiple relay processes, the deployment MUST propagate
@@ -494,9 +555,14 @@ mutation (step 7), simultaneously reserves the `(iss, jti)` replay identity and
inserts the deny entry — both or neither. A capacity failure at that step rejects
`503`; neither the jti nor the deny entry is recorded, and the caller may safely
retry the same signed command. On success, the relay closes all live connections
whose proven `k` equals `CommandResult.target_pubkey`. The `until` expiry is taken
exclusively from the signed command JWT claim; the request body carries no `until`
field. An unknown or unprovable pubkey is not an error; the relay responds `200`
registered under the command's issuer (`claims.iss`) whose proven `k` equals
`CommandResult.target_pubkey`, whether or not admission has completed.
Disconnect cancellation is terminal: a cancelled in-progress connection MUST NOT
be subsequently admitted. This close scan matches the same
`(iss, target_pubkey)` key as the deny entry; connections registered under
another issuer are not affected. The `until` expiry is taken exclusively from
the signed command JWT claim; the request body carries no `until` field. An
unknown or unprovable pubkey is not an error; the relay responds `200`
with `{"disconnected": true}`. An `until` value in the past is not an error;
sessions are closed. Absent an active same-key deny entry the past-`until`
creates no future denial; if an active entry already exists it remains unchanged
@@ -670,7 +736,7 @@ The following cardinality rules apply to all kind-24242 proofs:
Every kind-24242 proof MUST be subject to the full NIP-FI per-request pairing
requirement: full assertion verification, exact key equality between the
assertion's `nostr_pubkey` claim and the kind-24242 event's public key, and
deny-map enforcement (see Admission procedure, steps 1–5).
deny-map enforcement (see Admission procedure, steps 1–6).
The effectiveness of deny-map enforcement is contingent on the real
issuer-scoped deny map. Until that map is operational, the stub implementation
@@ -711,10 +777,14 @@ mixed-profile fields deny. [FI-TRACE-TRANSPORT-CLOSED]
On each protected HTTP request:
1. Extract `Nostr-Federated-Identity`; missing or malformed → deny
`missing_evidence` or `evidence_rejected`.
2. Call `VerifyAssertion`; any error → deny per the rejection table.
3. Validate the NIP-98 `Authorization` event per NIP-98; extract the proven
1. Resolve the connection's `Host` to `community`, including its canonical
`expected_aud` and configured issuer allowlist; unknown, ambiguous, or
unavailable community resolution → deny `authorization_unavailable`.
2. Extract the compact JWS `token` from `Nostr-Federated-Identity`; missing or
malformed → deny `missing_evidence` or `evidence_rejected`.
3. Call `VerifyAssertion(token, community, D, R_t)`; any error → deny per the
rejection table.
4. Validate the NIP-98 `Authorization` event per NIP-98; extract the proven
pubkey `k` from the event. An absent, malformed, or invalid NIP-98 event
→ deny `missing_evidence` or `evidence_rejected` as appropriate.
For requests with an authorization-relevant body, the NIP-98 event MUST
@@ -722,11 +792,11 @@ On each protected HTTP request:
SHA-256 hash of the exact consumed request body bytes. An absent,
duplicate, or mismatched `payload` tag on such a request → deny
`evidence_rejected`. [FI-TRACE-HTTP-INGRESS]
4. Assert `verified.asserted_key == k`; mismatch → deny `authorization_denied`.
5. Assert `verified.asserted_key == k`; mismatch → deny `authorization_denied`.
[FI-TRACE-ASSERTION-KEY-MISMATCH]
5. Check deny set for `(iss, k)`; active entry (`now < until`) → deny
6. Check deny set for `(iss, k)`; active entry (`now < until`) → deny
`authorization_denied`. [FI-TRACE-DENY-SET]
6. Admit the request.
7. Admit the request.
A body is **authorization-relevant** whenever any body byte influences the
authorization decision, target resource, requested capability, effect
@@ -759,14 +829,15 @@ free text, reason code, issuer, subject, key, claim, or timing hint.
Public class is a function only of evidence the requester supplied, never of
private per-principal server state; `authorization_unavailable` is the sole
exception and reveals only that a required dependency is unreadable.
exception and reveals only that a required dependency or community/Host mapping
is unavailable.
| Private condition | Public class | Nostr text | HTTP response |
|---|---|---|---|
| assertion or proof absent | `missing_evidence` | `auth-required: authentication required` | `401`; `WWW-Authenticate: Nostr`; `Content-Type: text/plain; charset=utf-8`; body `authentication required\n` |
| malformed, invalid, or expired evidence | `evidence_rejected` | `restricted: evidence rejected` | `403`; `Content-Type: text/plain; charset=utf-8`; body `evidence rejected\n` |
| assertion–key mismatch; local policy denial; active deny-set entry for pubkey | `authorization_denied` | `restricted: authorization denied` | `403`; `Content-Type: text/plain; charset=utf-8`; body `authorization denied\n` |
| required JWKS snapshot unreadable | `authorization_unavailable` | `restricted: authorization unavailable` | `503`; `Content-Type: text/plain; charset=utf-8`; body `authorization unavailable\n` |
| unknown or community-unauthorized issuer; malformed, invalid, or expired evidence | `evidence_rejected` | `restricted: evidence rejected` | `403`; `Content-Type: text/plain; charset=utf-8`; body `evidence rejected\n` |
| assertion–key mismatch; unauthorized issuer principal, signed-target–body mismatch, or replayed `jti` on a signed command; active deny-set entry for pubkey | `authorization_denied` | `restricted: authorization denied` | `403`; `Content-Type: text/plain; charset=utf-8`; body `authorization denied\n` |
| required JWKS snapshot or community/Host resolution unavailable | `authorization_unavailable` | `restricted: authorization unavailable` | `503`; `Content-Type: text/plain; charset=utf-8`; body `authorization unavailable\n` |
A denial decided on a WebSocket upgrade is the HTTP response in place of `101`.
A denial decided on a protected HTTP request is the HTTP response.
@@ -786,6 +857,12 @@ normative behavior for them:
- Identity↔key registry, key ownership records, and the one-identity one-key
constraint: issuer-side.
- Key rotation, re-enrollment after device loss: issuer-side.
- Per-session (`sid`-level) revocation: issuer-side. The relay's revocation
primitive is pubkey-scoped and community-wide, keyed `(iss, target_pubkey)`
(see Admin disconnect API). If an issuer revokes an entire identity, it
enumerates and disconnects each key it has asserted for that identity.
Individual session/device logout is handled by assertion expiry within the
configured TTL window.
- Revocation signaling to the issuer/IdP: issuer-side; the issuer stops
issuing assertions, which closes the relay window within assertion TTL.
- Directory integration and account-offboarding automation: issuer-side.
@@ -831,14 +908,14 @@ deployment-local identifiers. [FI-TRACE-DISCOVERY-PRIVATE]
| ID | Required outcome |
|---|---|
| `FI-TRACE-TRANSPORT-CLOSED` | Exact one-header input succeeds; missing, repeated, combined, malformed, and fallback variants deny. |
| `FI-TRACE-ASSERTION-VALIDATION` | Valid boundary input passes; each signature, key-selection, issuer, audience, time, size, and missing-configuration negative denies. |
| `FI-TRACE-ASSERTION-VALIDATION` | Valid boundary input passes; each signature, key-selection, issuer, audience, time, size, and missing-configuration negative denies; a missing or unavailable community configuration denies `authorization_unavailable`; a known-but-community-unauthorized issuer denies `evidence_rejected` without touching its key source, indistinguishably from an unknown issuer, including when that issuer's snapshot is unavailable; an issuer authorized only for community A cannot authorize community B merely by signing an assertion with community B's audience; an assertion for community A presented on a `Host` resolved to community B denies on the community binding; an unknown, ambiguous, or unresolvable `Host` denies `authorization_unavailable`. |
| `FI-TRACE-TOKEN-CLASS` | `at+jwt` and `nip-fi+jwt` pass only their selected class; ID tokens, wrong or generic types, and cross-class fallback deny. |
| `FI-TRACE-ASSERTION-KEY-MISMATCH` | Mismatch between `nostr_pubkey` and the NIP-42 proven key denies with the private-state response. |
| `FI-TRACE-JWKS-ADD` | A key added to the JWKS is accepted after the next snapshot refresh. |
| `FI-TRACE-JWKS-REMOVE` | Connections verified under a removed key deny on next revalidation or reconnect. |
| `FI-TRACE-DEPENDENCY-FAIL-CLOSED` | An unreadable JWKS snapshot denies `authorization_unavailable`; no degraded Nostr-only access. |
| `FI-TRACE-LEASE-BOUND` | A session closes at its earliest deadline; equality at any deadline is expired. |
| `FI-TRACE-DENY-SET` | A pubkey in the deny set is denied `authorization_denied` on admission until `now >= until`; an expired or absent entry does not deny; a past-`until` command closes sessions — absent an active same-key entry it creates no future denial, while an active entry remains unchanged under the merge rule; a deny-set-full command is rejected `503` without closing sessions and without removing any existing entry; capacity is evaluated per issuer — one issuer's capacity exhaustion MUST NOT reject another issuer's command; a connection that passes the deny-set check before a concurrent deny-entry insertion but completes admission after MUST still be terminated (the session's proven `k` is registered before the deny-set check, ensuring the close scan catches it); two overlapping commands for the same `(iss, pubkey)` in either delivery order result in `until = max(until_A, until_B)` — delivery order does not shorten the longer deny; a past-`until` command arriving over an active entry leaves the active entry's `until` unchanged; a successful disconnect responds `{"disconnected": true}` regardless of how many sessions were closed; the deny entry applies across all communities served by the relay under that issuer. |
| `FI-TRACE-DENY-SET` | A pubkey in the deny set is denied `authorization_denied` on admission until `now >= until`; an expired or absent entry does not deny; a past-`until` command closes sessions — absent an active same-key entry it creates no future denial, while an active entry remains unchanged under the merge rule; a deny-set-full command is rejected `503` without closing sessions and without removing any existing entry; capacity is evaluated per issuer — one issuer's capacity exhaustion MUST NOT reject another issuer's command; a connection that passes the deny-set check before a concurrent deny-entry insertion but completes admission after MUST still be terminated (the session's proven `k` and admitting issuer are registered before the deny-set check, ensuring the close scan catches it); a registered-but-not-admitted connection caught by the close scan MUST be cancelled terminally and MUST NOT later be admitted; a disconnect command from one issuer MUST NOT close a same-key connection registered under a different issuer, whether or not admission has completed; two overlapping commands for the same `(iss, pubkey)` in either delivery order result in `until = max(until_A, until_B)` — delivery order does not shorten the longer deny; a past-`until` command arriving over an active entry leaves the active entry's `until` unchanged; a successful disconnect responds `{"disconnected": true}` regardless of how many sessions were closed; the deny entry applies across all communities served by the relay under that issuer. |
| `FI-TRACE-HTTP-INGRESS` | A protected HTTP request with both valid headers and matching pubkeys is admitted; absent, mismatched, or invalid assertion or NIP-98 event denies; a request presenting only one of the two denies; an active deny-set entry denies; a route that cannot be classified as exempt is treated as protected; repeated, comma-combined, wrong-scheme, or alternative-credential `Authorization` fields deny `evidence_rejected`; a missing `Authorization` field denies `missing_evidence`; an authorization-relevant body without exactly one matching `payload` tag denies; the NIP-FI administrative API is not a protected surface. For kind-24242 (Blossom) proofs on media routes: a `t=upload` proof with valid `x`, `server`, `expiration`, and freshness is admitted on `PUT /upload`; a `t=get` proof with valid `server`, `expiration`, and freshness is admitted on `GET\|HEAD /media/{hash…}`; a kind-24242 proof on any other route denies; an upload proof with absent or mismatched `server` tag denies; a read proof with absent or mismatched `server` tag denies; a proof with a duplicate, missing, or out-of-range `expiration` tag denies; a proof dated more than 5 seconds in the future denies; a proof older than 60 seconds denies; key mismatch between assertion `nostr_pubkey` and the kind-24242 event pubkey denies; an active deny-set entry denies. |
| `FI-TRACE-DENIAL-ORACLE` | Each public-class row produces its exact fixed bytes; all private-state rows compare byte-identical. |
| `FI-TRACE-DISCOVERY-PRIVATE` | Complete discovery bytes do not expose issuer, audience, or deployment-private state. |
@@ -869,8 +946,13 @@ assertions. If the issuer continues issuing assertions, access continues.
For the deny-until-TTL disconnect model (issuer issues a successful disconnect
call with an `until` timestamp), the relay inserts a deny entry for the target
pubkey with expiry `until` and then closes all matching sessions synchronously. Any
subsequent admission attempt for that pubkey is denied `authorization_denied`
pubkey with expiry `until` and then closes all matching connections synchronously.
Here, matching means connections registered under that issuer whose proven `k`
equals that target pubkey, whether or not admission has completed; disconnect
cancellation is terminal, so a cancelled in-progress connection MUST NOT be
subsequently admitted. Connections registered under another issuer are not
affected. Any subsequent admission attempt for that pubkey under the same issuer
is denied `authorization_denied`
until `now >= until`. The `until` ceiling enforced by the relay is
`now + skew + maximum_assertion_age`; this limits how long a deny entry may last —
it does not ensure denial outlasts all live assertions. A short or past
@@ -916,4 +998,5 @@ is bounded before any attacker-controlled lookup.
- NIP-98 HTTP authorization: <https://github.com/nostr-protocol/nips/blob/ae0fd96907d0767f07fb54ca1de9f197c600cb27/98.md>
- JWT BCP: <https://www.rfc-editor.org/rfc/rfc8725>
- JWT access-token profile: <https://www.rfc-editor.org/rfc/rfc9068>
- Resource Indicators for OAuth 2.0: <https://www.rfc-editor.org/rfc/rfc8707>
- DPoP: <https://www.rfc-editor.org/rfc/rfc9449>