NIP-FI: Federated Identity Authorization (spec + formal model) - #1485
NIP-FI: Federated Identity Authorization (spec + formal model)#1485tlongwell-block wants to merge 4 commits into
Conversation
Normative spec for authorizing a NIP-42/NIP-98-proven Nostr key only when a valid OIDC/JWT assertion resolves to the same active identity-to-key binding. Covers assertion transport (trusted-proxy and client-attached profiles), validation, per-domain binding bijection, enrollment modes (attested-key/provisioned/tofu), lease/session bounds, revocation and rotation, delegation composition, rejection semantics, discovery, and privacy. Companion formal model to follow on this branch. Generalizes the semantics implemented in #1476 into a provider-neutral standard per buzz-security thread 0d1aee9e. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Define the trust assumptions, authorization predicate, binding state machine, safety and liveness properties, and adversarial traces for federated identity authorization. Tighten the normative draft with a collision-free client assertion header and concrete NIP-11 discovery shape. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Two cold-read consistency fixes: the model's race clause still said a lost race must deny, contradicting the spec's exact-match-allows rule; and the spec's session text offered 'require a fresh assertion' at lease expiry, which has no defined mid-connection transport — assertions only arrive on an upgrade request. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Remove the model's remaining impossible mid-connection assertion refresh option so its WebSocket transition exactly matches the normative transport. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
wesbillman
left a comment
There was a problem hiding this comment.
The revocation requirement is not enforced by the authorization transition. Revoke removes (i, k) from B_D and appends it to R_D, but Authorize only reads active bindings in B_D. On the next request for the same revoked pair, both lookups are empty, so attested-key and tofu recreate the pair and return ALLOW(created). That directly contradicts the normative text at NIP-FI.md:111 and the model's own requirement at NIP-FI-MODEL.md:137 that a fresh assertion must not reactivate a revoked pair without explicit recovery authorization.
Please make revocation history/tombstones part of the atomic authorization transition (for example, deny enrollment when (D, i, k) has a revocation record unless this invocation is an explicit recovery transition), and update the liveness statement so “unbound pair” excludes revoked pairs.
|
this would be a requirement for us (or anyone wanting to adhere to EU law) to use this software.. as its part of standard NIS2 and GDPR requirements to have sane security practices - incl. centralised user management (as allowed by OIDC or OAuth2) |
e81a8b7 to
805f113
Compare
…FECYCLE/DELEG/CONF) (#5946) ## What Comprehensive NIP-FI against `main`: one normative core plus four separately claimable profiles, replacing the single-document structure of #3726 (which was based on #1485's branch, not `main`). Six documents, 1,975 lines, docs only. - **`NIP-FI.md` (core, 632 lines)** — issuer-qualified identity `(iss, sub)`, independent Nostr proof, client-attached assertion, partial bijection with durable tombstones, atomic final admission, bounded leases, private denials with a closed response vocabulary, retire/revoke/rotate, two contract identities (`assertion_policy_id`, `transport_contract_id`), per-policy `skew` / `maximum_assertion_age` / `maximum_status_age` with missing configuration denying, a closed token-class rule (`at+jwt`, `nip-fi+jwt`, named compatibility; ID tokens always deny), declared freshness class (`offline-jwt` | `current-status`), server-declared body authorization relevance (NIP-98 payload-binding fix, including a `payload` tag on an irrelevant-body operation), BCP 14, "equivalent" defined over identity / bounds / provenance classes, a compact non-normative worked wire example, and a non-normative comparison with DPoP, mTLS-bound tokens, and HTTP Message Signatures. FI-INV-01..16 are normative core text. The behavioral-oracle table lists exactly 30 oracle IDs, one per row, with no shorthand. - **`NIP-FI-EDGE.md`** — trusted-edge surface: the `trusted-proxy-hmac-v2` envelope + canonicalization, or a private authenticated-edge adapter under a reviewed contract; `authorization_domain_id` derivation (exact 16 RFC 9562 UUID bytes); `proof_transport_code` registry (0x01 NIP-42, 0x02 NIP-98; 0x03 Git smart-HTTP and 0x04 Blossom reserved pending their transport contracts; 0x05–0x7f unassigned pending published stable specifications) + extension procedure; body-acquisition bounds; three normative test vectors. An independent Nostr proof (the NIP-98 event in `Authorization`, which reaches the verifier byte-identical, or the NIP-42 event after connect) is the only decision input outside the MAC; absent or incomplete provenance on an edge-required route is `missing_evidence`, present-but-failing provenance is `evidence_rejected`. Header-trust-without-provenance is nonconformant. - **`NIP-FI-LIFECYCLE.md`** — provision / disable / re-enable / administrative expiry (`binding_not_after`) / pending-replacement lineage, one conformance trace per privileged transition; every binding-creating transition declares whether it continues or establishes a grant; a private-condition table for CONF enumeration agreement. - **`NIP-FI-DELEG.md`** — delegated agents; explicit temporal boundaries matching core's inclusive-`nbf`/exclusive-`exp` idiom, with the delegated `skew` configured by this profile; lease deadline anchored to the lease issue instant; strict path separation — a delegated request carries no assertion or provenance field, so it cannot traverse an edge-provenance route and uses ingress on which NIP-FI-EDGE is not required. - **`NIP-FI-CONF.md`** — conformance evidence: an immutable claim tuple including the governing document revision and exit fixture digest; the complete 16-row denial-fixture enumeration with three mechanical enumeration-agreement checks; mutation adequacy with a countable denominator — one retained killed mutant per literal oracle-table row (30 core + 6 EDGE + 11 LIFECYCLE + 7 DELEG + 4 CONF = 58), rows selected structurally by their first cell, never by section title, with the release gate and CONF's own oracle rows stated in the same listed-oracle terms and a mutant defined for CONF's own report- and suite-subject oracles; an interoperability exit test compared over signing inputs (per-transport NIP-01 serialization for the NIP-98 and NIP-42 proofs; decoded protected header and claims as JSON values for the assertion), with a shared exit fixture pinning complete pre-signature header/claim JSON and complete unsigned event fields for both transports, and mandatory negative controls. `FI-CONF-INTEROP-EXIT` is `deferred` with reason `no-independent-implementation` until a second independent implementation exists; the canonical fixture is editor-authored at `docs/nips/fixtures/nip-fi-conf-exit.json` and is **not in this PR** — until it is published a claim records `pending-canonical-fixture`, valid only while the exit test is deferred. Explicit not-applicable dispositions, including `offline-jwt` deployments for the two current-status oracles and absence of a revocation-bounded external capability projection for `FI-TRACE-CAPABILITY-REVOCATION`. - **`NIP-FI-MODEL.md`** — non-normative companion; defines no requirement or conformance claim and is not claimable. ## Why The prior draft rated 9 (soundness) / 6 (minimalness) / 7 (elegance) / 7 (correctness) in adversarial + comparative review. This restructure keeps the two-invariant spine untouched, makes everything else a claimable profile, and collapses five stacked versioning mechanisms into two contract identities. Mutation adequacy counts one mutant per literal oracle-table row — a set two implementers enumerate identically — instead of "each normative requirement," which had four defensible readings. Resolved product calls (owner-approved): 1. Enrollment/denial posture is private — boolean enrollment discovery, TOFU extension claim not self-advertised, `key_mismatch → authorization_denied` joins the denial anonymity set, and replayed evidence is classed `authorization_denied` so resubmission reveals nothing about commit. 2. Revocation honesty — only `current-status` deployments may advertise an unconditional residual-revocation bound; `offline-jwt` advertises unbounded/unknown. Access tokens keep RFC 9068 `at+jwt`; `nip-fi+jwt` is reserved for a separately minted Buzz assertion. ## Acceptance bar - Nothing in core is deletable without losing a stated core guarantee. - From the core document plus the CONF exit fixture, a second implementer can produce a valid request equal over the request compared object (signing inputs), and a byte-exact public denial per class — no reference implementation. - Every oracle-table row ships a retained killed mutant satisfying only the entry it was selected for. - Both deployment profiles (trusted proxy = EDGE, client-held OIDC = core client-attached) pass the same lifecycle conformance suite. ## Status Ready at head e720a5c. Every revision below is on this branch: the 2026-08-17 and 2026-08-18 review laps (Wren, Dawn, Perci, Sami, Mari, Quinn) closed at 513e03b, 17d455a, and 4f913a8; the 2026-08-20 external line-by-line review (R1–R10, R12) closed across 56e7414..772ba7a; the 2026-08-20/21 adversarial lap (#6437) squash-merged as b8db13d; the round-3 external review (R13, R14), the DELEG×EDGE composition note, and three terminology nits closed at e720a5c; R11 is this description. Oracle census: 58 (30 core, 6 EDGE, 11 LIFECYCLE, 7 DELEG, 4 CONF). Known follow-ups, filed after merge and out of scope here: adapter-only edge deployments and FI-EDGE claimability; an EDGE private-condition table for CONF's enumeration-agreement check; an enumerable definition of the positive/negative oracle sets used by the global mutation controls; NIP-OA's clock-free verification versus NIP-FI-DELEG's wall-clock expiry. Supersedes #3726 as the spec vehicle; #1485 remains the design-history anchor. --------- Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Signed-off-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Signed-off-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Signed-off-by: Dawn <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@buzz.block.builderlab.xyz> Signed-off-by: Tyler Longwell <tlongwell@squareup.com> Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz> Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz> Co-authored-by: Dawn <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@buzz.block.builderlab.xyz> Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
What
Adds NIP-FI: Federated Identity Authorization — a normative spec (
docs/nips/NIP-FI.md) and companion formal model (docs/nips/NIP-FI-MODEL.md) for authorizing an already-authenticated Nostr key only when a valid OIDC/JWT assertion from an external identity provider resolves to the same active identity-to-key binding.Docs only; no code changes.
Why
#1476 implements corporate identity binding for the Buzz relay. Per the buzz-security discussion (thread
0d1aee9e), the requirement is broader: anyone with Okta or a generic OAuth2 reverse proxy providing JWTs should be able to enable this class of enhanced identity auth. Without a spec, the first large deployment's configuration becomes an accidental standard. Prior-art survey (upstreamnips@8f8444d, strfry, Chorus, nostr-rs-relay, rnostr, relay.tools, NIP-46/98, Blossom) found no existing NIP or relay implementation binding a trusted IdP subject to a pubkey at relay authentication — closest shapes are NIP-05 domain verification (DNS/HTTPS polling, not request-time credentials) and NIP-46auth_url(custody UX, not relay binding).Design spine
(iss, sub)— never baresub, username, or email.attested-key(IdP carries the npub claim),provisioned(admin-created only), risk-labeledtofu.exp, tracked per authenticated key.Nostr-Federated-Identity: Bearer <JWT>(avoiding NIP-98'sAuthorizationheader).limitation.federated_identity+ optionalfederated_identityobject.Relationship to #1476
The spec generalizes #1476's semantics (reviewed at
bd822f3e) into a provider-neutral contract. One known delta: the spec requires WS leases not to outlive assertionexp; #1476 PR1 checks identity once at AUTH and defers lease expiry to its lifecycle follow-up. Spec leads implementation here.Verification