From 26ede6dfa2496993aa62ce5781d2112df4c2d009 Mon Sep 17 00:00:00 2001 From: Will Pfleger Date: Tue, 22 Sep 2026 17:56:45 -0400 Subject: [PATCH] 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 Signed-off-by: Alia Signed-off-by: Paul <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@buzz.block.builderlab.xyz> Co-authored-by: Alia Co-authored-by: Paul <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@buzz.block.builderlab.xyz> --- docs/nips/NIP-FI.md | 197 +++++++++++++++++++++++++++++++------------- 1 file changed, 140 insertions(+), 57 deletions(-) diff --git a/docs/nips/NIP-FI.md b/docs/nips/NIP-FI.md index 1444d36dda..fee7909dfd 100644 --- a/docs/nips/NIP-FI.md +++ b/docs/nips/NIP-FI.md @@ -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` and `ProductionJwksSource` -(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: - JWT BCP: - JWT access-token profile: +- Resource Indicators for OAuth 2.0: - DPoP: