mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user