Skip to content

NIP-FI: Federated Identity Authorization (spec + formal model) - #1485

Open
tlongwell-block wants to merge 4 commits into
mainfrom
nip-fi-federated-identity
Open

NIP-FI: Federated Identity Authorization (spec + formal model)#1485
tlongwell-block wants to merge 4 commits into
mainfrom
nip-fi-federated-identity

Conversation

@tlongwell-block

@tlongwell-block tlongwell-block commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

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 (upstream nips @ 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-46 auth_url (custody UX, not relay binding).

Design spine

  • Identity is issuer-qualified (iss, sub) — never bare sub, username, or email.
  • Active bindings form a per-domain partial bijection between identity and key; every transition preserves it.
  • A bearer assertion never substitutes for Nostr proof (NIP-42/NIP-98); the authorized key is always the proof's key.
  • Explicit enrollment modes: attested-key (IdP carries the npub claim), provisioned (admin-created only), risk-labeled tofu.
  • Check + first bind is atomic; races and storage failures fail closed.
  • Cached WS authorization is a lease bounded by assertion exp, tracked per authenticated key.
  • Revocation cannot be undone by replaying a still-valid assertion; key rotation is an explicit transition, never conflict resolution.
  • Trusted-proxy transport is conforming only when direct ingress is impossible and inbound header copies are stripped; client-attached transport uses Nostr-Federated-Identity: Bearer <JWT> (avoiding NIP-98's Authorization header).
  • Delegation (e.g. NIP-OA) composes without granting the delegate a federated binding.
  • Discovery via NIP-11 limitation.federated_identity + optional federated_identity object.

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 assertion exp; #1476 PR1 checks identity once at AUTH and defers lease expiry to its lifecycle follow-up. Spec leads implementation here.

Verification

  • Formal model: trust assumptions, authorization predicate, binding state machine, safety/liveness properties, 13 adversarial traces — each trace resolves deterministically against normative text.
  • Joint pre-PR quality gate (50+ binary completeness checks, benchmark vs NIPs 05/42/46/98): run independently by both authors; all Minimalness/Elegance/Correctness scores ≥9/10.

npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d and others added 4 commits July 2, 2026 20:28
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 wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@KlavsKlavsen

Copy link
Copy Markdown

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)

@cameronhotchkies cameronhotchkies added the triage-ready Appropriate for agentic review label Jul 30, 2026
@cea-block
cea-block requested a review from a team as a code owner August 9, 2026 16:02
@cea-block
cea-block force-pushed the nip-fi-federated-identity branch from e81a8b7 to 805f113 Compare August 11, 2026 19:53
tlongwell-block added a commit that referenced this pull request Aug 21, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage-ready Appropriate for agentic review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants