docs(nips): comprehensive NIP-FI — core + claimable profiles (EDGE/LIFECYCLE/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 e720a5cda. 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 513e03b06, 17d455a9b, and 4f913a813; the 2026-08-20 external
line-by-line review (R1–R10, R12) closed across 56e74141e..772ba7a72;
the
2026-08-20/21 adversarial lap (#6437) squash-merged as b8db13d88; the
round-3
external review (R13, R14), the DELEG×EDGE composition note, and three
terminology nits closed at e720a5cda; 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>
This commit is contained in:
Tyler
2026-08-21 17:18:24 -04:00
committed by GitHub
parent 025425591e
commit d97780b477
6 changed files with 1975 additions and 0 deletions

375
docs/nips/NIP-FI-CONF.md Normal file
View File

@@ -0,0 +1,375 @@
NIP-FI-CONF
===========
Conformance evidence profile
----------------------------
`draft` `optional`
**Dependencies**: NIP-FI core. Applies additionally to any claimed
NIP-FI-EDGE, NIP-FI-LIFECYCLE, and NIP-FI-DELEG profile.
The key words "MUST", "MUST NOT", "REQUIRED", "SHOULD", "SHOULD NOT", and
"MAY" in this document are to be interpreted as described in BCP 14 (RFC 2119
and RFC 8174) when, and only when, they appear in all capitals.
## Abstract
NIP-FI core and its profiles state required behavior. This profile states what
counts as evidence that an implementation has it: the claim unit, the evidence
rules, the complete denial-fixture enumeration, mutation adequacy, and the
interoperability exit test.
This profile is separately claimable and is never advertised in discovery:
conformance is a property of a reviewed revision, not a wire feature. It
defines no wire behavior, denial mapping, invariant, or admission rule; where
it names one, NIP-FI core or the owning profile is normative.
## Claim unit
A conformance claim names exactly one immutable tuple:
```text
(implementation revision,
adapter revision,
build artifact digest,
deployment revision,
governing document revision,
exit fixture digest,
claimed profiles,
assertion_policy_id,
transport_contract_id,
enrollment mode)
```
Changing any element creates a new claim. Results from one tuple MUST NOT be
carried into another. A report contains every applicable oracle from core and
every claimed profile exactly once, with status `pass` or `not-applicable`
only, except that `FI-CONF-INTEROP-EXIT` alone may instead carry `deferred`
under the condition in **Interoperability exit test**. Blank, skipped,
expected-failure, and not-run results cannot support a claim
(`FI-CONF-CLAIM-COMPLETE`).
Enrollment mode is part of the claim unit and is private: it is recorded in
the access-controlled report, never in discovery or any public artifact.
## Evidence rules
Each passing oracle records the claim tuple, a stable test identifier and
adapter entry point, the command with start time, end time, exit status, and
any random seed, the synthetic input or a privacy-safe digest of it, the
before-and-after authoritative state relevant to the oracle, the expected and
observed outcomes, and artifact locations with SHA-256 digests. Stateful
oracles use an isolated database or namespace and inspect committed state
rather than inferring it from a response. Concurrency oracles record every
contender and the single serialized outcome. Time-boundary oracles use a
controlled clock.
Adapters MUST drive public or production-equivalent entry points. A storage
helper MAY inspect state or inject a dependency outage; it MUST NOT replace the
operation under test. Calling an internal authorization function without
traversing the protected ingress does not satisfy ingress coverage.
None of the following satisfies any oracle: searching source, documentation,
schemas, or binaries for a token; asserting that a route calls a named
function; recording a test name without its execution result; using a mock to
prove a deployed network boundary; citing a check from another revision; or
marking an oracle passed because the feature is configured.
`FI-TRACE-TOFU-THEFT` takes an access-controlled **configuration** witness
only; under the private-posture rule no discovery witness for enrollment mode
can exist. Discovery invariance is proved separately by
`FI-TRACE-DISCOVERY-PRIVATE`.
Requirements marked `[deployment artifact: ...]` in core or a profile are
evidenced by the named access-controlled review record at the claimed
deployment revision, not by a behavioral oracle. A claim listing an artifact
without the record is incomplete.
Reports and artifacts hold private deployment detail and MUST remain access
controlled. They MUST NOT enter public reports, examples, discovery, or
protocol output, and MUST NOT contain raw assertions, secrets, or unredacted
`iss`, `sub`, or claim values. The shared exit fixture is exempt: its values
are synthetic by construction and name no real principal, issuer, or key.
## Denial fixtures
`FI-TRACE-DENIAL-ORACLE` requires one fixture per **private condition**, not
one per public class; a per-class suite compares a class against itself. The
enumeration below is the required fixture set (`FI-CONF-DENIAL-FIXTURES`). The
public-class column restates NIP-FI core, which owns the mapping and the bytes.
| # | Private condition | Public class | Defined by |
|---|---|---|---|
| 1 | assertion, proof, or delegation evidence absent | `missing_evidence` | core |
| 2 | edge provenance absent or incomplete on an edge-required route (assertion may be present) | `missing_evidence` | NIP-FI-EDGE |
| 3 | evidence present but rejected: signature, key selection, issuer, audience, time, size, ambiguity, token class, body binding, or edge provenance (present but rejected) | `evidence_rejected` | core, NIP-FI-EDGE |
| 4 | replayed evidence — committed replay identity already claimed | `authorization_denied` | core, NIP-FI-EDGE |
| 5 | `key_mismatch` — asserted key is not the proven actor | `authorization_denied` | core |
| 6 | `attestation_required` — attested-key enrollment without a matching key claim | `authorization_denied` | core |
| 7 | `binding_conflict` — either side of the active relation is taken | `authorization_denied` | core |
| 8 | `pair_retired` | `authorization_denied` | core |
| 9 | `key_revoked` | `authorization_denied` | core |
| 10 | `policy_denied` — local operation policy | `authorization_denied` | core |
| 11 | `binding_required` — enrollment policy creates no binding at this request: provisioned mode with no binding, or any unrecognized policy value | `authorization_denied` | core |
| 12 | `identity_disabled` | `authorization_denied` | NIP-FI-LIFECYCLE |
| 13 | `explicit_replacement_required` — pending lineage | `authorization_denied` | NIP-FI-LIFECYCLE |
| 14 | `binding_expired` — administrative expiry | `authorization_denied` | NIP-FI-LIFECYCLE |
| 15 | `delegation_not_current` — owner or relationship no longer current | `authorization_denied` | NIP-FI-DELEG |
| 16 | `dependency_unreadable` | `authorization_unavailable` | core |
Private-condition names are fixture identifiers, not wire values; a deployment
MAY use other internal reason codes if every enumerated condition has a
fixture. Rows for an unclaimed profile are `not-applicable` with absence
evidence. A profile that introduces a private condition MUST add its row; an
unenumerated condition escapes this oracle entirely.
**Enumeration agreement.** `policy_denied` and `dependency_unreadable` are the
*prose-only allowlist*: core conditions that core states in prose and does not
name symbolically. The suite MUST check mechanically at the claimed head, by
symbol and never by row number, and every check MUST be green on the unmutated
documents before any mutant is scored:
1. every symbol core denies by name has a row here attributed to core with the
same public class;
2. the set of symbols in core-attributed rows equals core's symbolic denial set
together with the allowlist, exactly, and the allowlist is disjoint from
that set; and
3. for each claimed profile that owns a private-denial-condition table, the
set of `(identifier, public class)` pairs in that table equals the set of
pairs attributed to that profile here, exactly; a row with multiple owners
contributes its pair to each.
If a later core names an allowlisted symbol, check 2's disjointness fails
until the allowlist entry is deleted, and check 1 validates the promoted
symbol's class.
**Anonymity comparison.** Every `authorization_denied` row is in the
private-state anonymity set. Between two private conditions on one
implementation, every response byte as transmitted MUST agree — transfer
framing included — except values a server cannot hold constant across two
instants, such as `Date`. This is wider than the interoperability object
below: within one implementation, any byte that varies by private condition is
a disclosure, whatever field it sits in.
**Interoperability compared object.** Between two implementations, comparison
is over what core pins and nothing more. Over Nostr: the complete relay message
excluding only the event or subscription identifier echoed from the request,
as compact JSON with no insignificant whitespace per NIP-01. Over HTTP: the
status code; the content per RFC 9110 Section 6.4, after transfer decoding with
chunk framing and trailers excluded; and the exact values of only the header
fields core's denial table names, field names matched case-insensitively per
RFC 9110 Section 5.1. `Content-Length` is not pinned. Header order and unnamed
fields are outside the object, and their values MUST NOT depend on the private
condition. A field core names that an implementation cannot hold constant MUST
be reported with the reason, and its value MUST be independent of the private
condition. If core later pins another field, it joins with no edit here.
**Run discipline.** The oracle runs a fixed positive iteration count on a
pinned isolated runner at the exact claimed head. Before the run the operator
records the environment, public-response corpus, bounds, sampling method,
statistical rule, noise treatment, and acceptance threshold. A breach fails the
gate, MUST NOT trigger an automatic retry, and is retained and investigated
before a separately authorized rerun.
`authorization_unavailable` is observably distinct from `authorization_denied`.
This is accepted residual: it discloses no per-principal state, and collapsing
it would make fail-closed behavior undiagnosable.
**Negative control.** The suite MUST include an implementation deliberately
patched to vary its denial response by private condition, and it MUST fail
this oracle.
## Mutation adequacy
An oracle that cannot fail is untested text that reads as tested. The
denominator is the **listed oracle**: every table row whose first cell names
exactly one complete literal oracle identifier, in NIP-FI core, in each claimed
normative profile, and in this document when CONF is claimed — selected by
that cell, not by section title. It is not the set of normative sentences, RFC
2119 keywords, or invariant labels, none of which two readers enumerate alike.
For each listed oracle the suite MUST retain at least one **mutant**: an
implementation variant that violates a requirement that oracle governs,
together with that oracle's failing output (`FI-CONF-MUTATION`). Evidence is
the exact patch identity, the oracle identifier, and the retained failure
output at the claimed head. For this document's own oracles the implementation
under test includes the conformance suite and its report; a mutant is a single
variant of the suite or report that the entry's own oracle rejects.
While `FI-CONF-INTEROP-EXIT` is validly deferred it remains in claim
completeness but is excluded from this section's mutation and global-control
obligations, since its failing output cannot exist without the run. Both
obligations attach with the run and MUST be discharged before either
implementation's interoperable conformance claim is accepted. No other
oracle's obligation under this section is deferrable.
Normative prose outside the oracle tables remains binding but is not a second
denominator. Prose that no listed oracle can detect is untestable text: add the
oracle that detects it, or delete it.
1. **One at a time.** Mutants are applied singly against an otherwise
unmodified implementation, so layered defenses cannot mask each other.
2. **Attribution.** The kill MUST come from the entry's own oracle. A mutant
killed only by another oracle establishes coverage for neither.
3. **One entry per mutant.** A mutant satisfies only the entry it was selected
for, even when it also kills other oracles.
4. **Reachability.** The suite MUST witness that a fixture reaches the mutated
decision, not merely the enclosing operation.
5. **Survivors are recorded.** A mutant its named oracle fails to kill is a
defect in the specification or the suite. It is recorded with that
disposition and MUST NOT be waived or replaced by an easier mutant.
Two global controls bound the suite. A deny-everything implementation MUST
fail every positive oracle; an allow-everything implementation MUST fail every
negative oracle. Neither substitutes for per-entry mutants.
## Interoperability exit test
A claim of core conformance requires evidence that the documents alone are
sufficient to build against (`FI-CONF-INTEROP-EXIT`). Two implementations that
have not shared code and have not consulted a common reference implementation
each produce, from NIP-FI core and any claimed profile documents alone:
- one valid `client-attached` request, over WebSocket upgrade and over HTTP,
compared over its signing inputs as defined below; and
- one byte-exact public denial response for each of the four public classes,
on each transport where the class can be decided, compared over the
interoperability compared object under **Denial fixtures**.
Independence is a claim about code, not inputs: two implementations given
different issuers, keys, or clocks cannot produce equal bytes. The run is
therefore parameterized by a **shared exit fixture** that both sides load and
neither side authors:
- one issuer identity and one JWK set, including the private key needed to
mint assertions and the `kid` selecting it;
- one assertion per denial class and one for the valid request, each as
complete pre-signature protected-header and claim-set JSON values —
including `alg`, `typ`, `kid`, every member the policy allows, and fixed
`iss`, `sub`, `aud`, `nostr_pubkey`, `client_id`, `iat`, `exp`, and token
class;
- one Nostr secret key for the proof, with the complete unsigned event fields
for each transport — the NIP-98 event over HTTP and the NIP-42 event with
its challenge and relay values over the WebSocket upgrade — including
`created_at`;
- one frozen evaluation instant, and the skew and lifetime bounds in force;
and
- the domain, target resource, operation, and enrollment policy for each case.
The canonical fixture is authored by this document's editors, not by any
claiming implementation, and MUST be published as a single file at
`docs/nips/fixtures/nip-fi-conf-exit.json` in the same repository as these
documents, with its SHA-256 digest, before any `FI-CONF-INTEROP-EXIT` run.
Both sides MUST load that file, MUST verify the digest before the run, and
MUST record the digest with the evidence; a run against any other fixture
instance is not `FI-CONF-INTEROP-EXIT` evidence. While the canonical fixture
is unpublished, the claim tuple's exit fixture digest records the reserved
value `pending-canonical-fixture`, valid only in a claim whose
`FI-CONF-INTEROP-EXIT` result is `deferred`. Publication changes the element
and therefore creates a new claim.
**Request compared object.** Signature octets are excluded, because conforming
implementations need not agree on them (randomized `ES256` and fresh-aux
BIP-340 do not) and no document here pins JWS or JSON member order. The
compared object is the **signing inputs**: for each transport's Nostr proof,
the NIP-01 serialization the event id is taken over, compared against its own
transport's serialization; for the assertion, the decoded protected header and
claim set compared as JSON values with member order excluded. Every value the
compared object depends on MUST be pinned in the fixture.
The exchanged artifact per case is the complete request and response frame on
each transport — for HTTP the request line, headers, and body and the response
status, headers, and body; for Nostr the complete client and relay messages —
so that a mismatch can be explained from fields outside the compared object.
The test passes when outputs compare equal over their compared objects and
each implementation accepts the other's valid request and reproduces the
other's denials. Exit evidence includes the exchanged artifacts and each
implementation's statement of independence. A divergence traced to an
underspecified value is a defect in the specification, not in either
implementation, and is fixed there.
**Negative control.** One implementation is patched to emit a denial that
differs from the other only outside the compared object — a header core does
not name, or reordered fields — and the run MUST still pass. A run that fails
this control is comparing more than core pins; the exit test is then the
defect. The control is retained with the evidence.
`FI-CONF-INTEROP-EXIT` is REQUIRED only once a second implementation meeting
the independence conditions exists. Until then a conformance claim MUST record
it as deferred with the machine-readable reason
`no-independent-implementation`. A deferred exit test MUST be run and passed
before the second implementation's conformance claim is accepted, and the
first implementation's claim MUST be re-evidenced against that run.
## Applicability
`not-applicable` requires a machine-readable reason and behavioral proof that
the surface is absent:
- edge oracles only when no trusted-edge profile is accepted, none is
advertised, and executable cases reject every trusted-edge evidence shape;
- snapshot-rotation oracles only when no local key or status snapshot source
is configured and executable evidence proves the absence;
- `FI-TRACE-TOFU-THEFT` only when TOFU is neither configurable nor configured
and executable first-use cases deny;
- `FI-TRACE-CURRENT-STATUS-STALE` and `FI-TRACE-CURRENT-STATUS-REVOKED` only
when every configured assertion policy declares freshness class
`offline-jwt` and executable cases prove a presented witness is never
consulted;
- `FI-TRACE-CAPABILITY-REVOCATION` only when no external capability
projection requiring a declared revocation bound is configured, and
executable evidence proves no assertion capability or local-policy value
claims such a bound;
- lifecycle and delegation oracles only when the profile is unclaimed,
disabled, and denied on every ingress; and
- every other oracle is required for an enforcing deployment.
An implementation that supports an optional surface runs its oracles even when
one deployed domain does not activate it.
## Release gate
Before NIP-FI enforcement or discovery is enabled, reviewers verify, at one
reviewed revision, that:
- one immutable claim tuple passes every applicable oracle other than a
validly deferred `FI-CONF-INTEROP-EXIT`;
- if the canonical fixture was published before the review, the tuple's exit
fixture digest is not `pending-canonical-fixture`;
- the protected-ingress inventory has no uncovered or competing authority;
- every listed oracle, other than a validly deferred `FI-CONF-INTEROP-EXIT`,
has a killed, attributed, reachable mutant and every survivor is recorded;
- the denial-fixture enumeration is complete for the claimed profiles and its
negative control fails as required;
- the interoperability exit test has passed against an independent
implementation, or is recorded as deferred because none exists;
- every named deployment artifact exists at the claimed deployment revision;
and
- public and operational sinks pass privacy-canary inspection.
Documentation review, source review, and static scans are review inputs. They
close no item in this gate.
## Behavioral oracles
| ID | Required outcome |
|---|---|
| `FI-CONF-CLAIM-COMPLETE` | A report missing an applicable oracle, duplicating one, carrying a result from another claim tuple, claiming a status other than `pass`/`not-applicable` — or `deferred` on any oracle other than `FI-CONF-INTEROP-EXIT` — or omitting mutant evidence for any oracle other than a deferred `FI-CONF-INTEROP-EXIT`, or recording the exit fixture digest `pending-canonical-fixture` with any `FI-CONF-INTEROP-EXIT` result other than `deferred`, is rejected. |
| `FI-CONF-DENIAL-FIXTURES` | Every enumerated private condition has a fixture; core and each claimed profile pass exact identifier/class/owner enumeration agreement; anonymity-set responses compare byte-identical; the distinguishing negative control fails. |
| `FI-CONF-MUTATION` | Every listed oracle — except `FI-CONF-INTEROP-EXIT` while validly deferred, per **Mutation adequacy** — has a singly-applied, attributed, reachability-witnessed mutant killed by that entry's own oracle; the deny-everything and allow-everything global controls fail every oracle **Mutation adequacy** requires of them, with retained evidence; survivors are recorded, not waived. |
| `FI-CONF-INTEROP-EXIT` | Two independent implementations produce, from the documents alone, valid requests equal over the request compared object and per-class denials equal over the denial compared object, and accept each other's output. |
## Security considerations
Conformance evidence is a privileged artifact: it enumerates private denial
conditions, enrollment posture, and deployment topology that the protocol
deliberately keeps off the wire. Publishing a report, a fixture corpus, or a
mutant catalogue would disclose exactly what `FI-INV-13` and
`FI-TRACE-DISCOVERY-PRIVATE` protect.
A passing suite bounds the behaviors it exercises and nothing else. Mutation
adequacy raises the cost of a masked defect; it does not prove absence of
defects, and a claim that cites this profile as proof of security rather than
of tested behavior is misusing it.

186
docs/nips/NIP-FI-DELEG.md Normal file
View File

@@ -0,0 +1,186 @@
NIP-FI-DELEG
============
Delegated agent authorization profile
--------------------------------------
`draft` `optional` `relay`
**Protocol dependency**: NIP-FI core.
The key words "MUST", "MUST NOT", "REQUIRED", "SHOULD", "SHOULD NOT", and
"MAY" in this document are to be interpreted as described in BCP 14 (RFC 2119
and RFC 8174) when, and only when, they appear in all capitals.
## Scope
This profile authorizes a delegate key from separately validated delegation
evidence rooted in a currently eligible NIP-FI owner binding. The delegate
proves its own key. It does not present a federated assertion and never receives
or inherits the owner's binding. Because a trusted edge inserts assertion and
provenance fields on every request it forwards, and `FI-DELEG-PATH-SEPARATION`
denies any such field on a delegated request, delegated requests cannot traverse
a route that requires edge provenance; they use ingress on which NIP-FI-EDGE is
not required.
This profile defines the normalized delegation result and its additional
preparation, final-admission, and lease witnesses. It does not define a wire
format for creating delegation relationships; NIP-OA or another protocol may
supply the evidence if it satisfies this contract.
## Delegation evidence
A validator returns this closed result:
```text
DelegationEvidence = (
domain,
owner_key,
delegate_key,
relationship_id,
relationship_revision,
audience,
operations,
conditions,
resource_or_target,
not_before?,
mandatory_expiry
)
```
`relationship_id` and `relationship_revision` are deployment-local dependency
identifiers. All other fields are interoperability-critical in meaning even
when their concrete encoding belongs to the supplying delegation protocol.
The evidence authenticates every field, has one unambiguous owner and delegate,
matches the server-owned domain and exact request or target, and has a finite
expiry satisfying `now < mandatory_expiry`; equality at an expiry is expired.
Optional `not_before` satisfies `not_before <= now + skew`, using the
configured delegated `skew`; arithmetic is overflow-safe. A missing
configured `skew` denies. The proven actor equals `delegate_key`.
[FI-DELEG-EVIDENCE-CLOSED]
A delegated request carries fresh request-appropriate Nostr proof and no
`Nostr-Federated-Identity` or profile provenance field. Mixed direct and
delegated evidence denies rather than selecting a path. [FI-DELEG-PATH-SEPARATION]
## Private denial conditions
This profile defines exactly this private condition identifier and owning public
class for NIP-FI-CONF enumeration agreement:
| Private condition identifier | Public class |
|---|---|
| `delegation_not_current` | `authorization_denied` |
The identifier is a fixture name, not a wire value. Adding, removing, renaming,
or reclassifying it requires the same change in NIP-FI-CONF's denial-fixture
table.
## Preparation
Preparation resolves the exact server-owned domain, target context, operation,
resource, and proven delegate actor before validating delegation evidence. It
then atomically reads:
- the active owner binding and exact binding version;
- every owner tombstone, key-revocation, administrative, and profile lifecycle
gate applicable to that binding;
- the exact relationship identifier and revision;
- current local policy and resource versions; and
- every invalidation dependency and deadline.
The owner binding is current and authorization-eligible at preparation. A
cached owner lease is not authority. The requested capability is the
intersection of the delegation's operation, audience, conditions, and target
with current local policy; an unsupported operation or empty intersection
denies. [FI-DELEG-OWNER-CURRENT]
Preparation remains read-only under `FI-INV-08`. It cannot create or change an
owner or delegate binding, identity, provenance, lifecycle fact, relationship,
last-seen value, replay claim, receipt, lease, or application effect.
[FI-DELEG-NO-BINDING]
## Final admission
Core final admission additionally requires:
1. the exact delegation evidence and delegate proof remain live;
2. domain, actor, target, audience, operation, resource, and relationship match
the prepared value;
3. the exact current owner binding and binding version remain eligible;
4. relationship identity and revision remain current;
5. current capability intersection equals the prepared intersection; and
6. changed dependencies are reread and the complete delegated decision is
recomputed before atomic commit.
Any mismatch, expiry, owner retirement, owner key revocation, owner binding
version change, relationship change, unreadable dependency, or unsupported
capability denies. Rotation makes the former owner key non-current; its
relationships do not transfer to the new key. [FI-DELEG-OWNER-CURRENT]
The delegated path creates no owner or delegate binding and cannot consume an
enrollment opportunity. Its receipt identifies the delegate actor and exact
owner-binding and relationship dependencies without publishing identity
material. [FI-DELEG-NO-BINDING]
## Delegated leases
A deployment configures a positive finite delegated maximum and a non-negative
finite delegated `skew`. The lease deadline is no later than the minimum of:
- delegation expiry;
- delegate proof or connection bound;
- owner binding administrative bound, when applicable;
- current relationship bound;
- local policy bound;
- the lease issue instant plus the configured delegated maximum; and
- any stronger owner-assertion bound the deployment requires.
Missing finite configuration denies. Equality is expired and arithmetic is
overflow-safe. [FI-DELEG-LEASE-BOUND]
Before each protected use, the service checks the delegate actor, owner binding
and version, relationship and revision, capability intersection, target,
resource, local policy, deadline, and invalidation state. It closes or rejects
the lease within the deployment's tested revocation-detection bound after any
owner or relationship dependency becomes ineligible. The claimed bound is no
smaller than measured worst-case detection plus enforcement delay.
[FI-DELEG-INVALIDATION-BOUND]
Owner retirement, revocation, rotation, disablement under NIP-FI-LIFECYCLE, or
binding replacement invalidates dependent delegates on the same effective
schedule as owner authority. A delegate lease never authorizes another delegate
or owner key on the same connection. [FI-DELEG-OWNER-CURRENT]
## Discovery
A relay claiming this profile MAY add `"delegation": true` to the NIP-11
`federated_identity` object only when owner-current resolution, the positive
finite maximum, uniform final admission, and all profile oracles are active. It
does not advertise relationship IDs, owner keys, private delegation protocol
names, or policy detail. [FI-DELEG-DISCOVERY]
## Behavioral oracles
| ID | Required outcome |
|---|---|
| `FI-DELEG-EVIDENCE-CLOSED` | Valid closed evidence passes; unauthenticated, ambiguous, wrong-domain/actor/target/audience, not-yet-valid, and expiry-equality variants deny. |
| `FI-DELEG-PATH-SEPARATION` | Delegation plus any direct assertion/provenance field denies; neither path falls back to the other. |
| `FI-DELEG-OWNER-CURRENT` | Exact current owner succeeds; retirement, revocation, rotation, replacement, stale owner version, stale relationship, and unreadable owner state deny without inheritance. |
| `FI-DELEG-NO-BINDING` | Successful, denied, and concurrent delegated requests create or change no owner/delegate binding or lifecycle state. |
| `FI-DELEG-LEASE-BOUND` | Every authority bound and equality boundary closes the lease; absent finite maximum denies. |
| `FI-DELEG-INVALIDATION-BOUND` | Measured owner/relationship revocation closes prepared evidence and live leases within the claimed detection bound. |
| `FI-DELEG-DISCOVERY` | Discovery is false/absent until the complete active profile passes; public output contains no relationship or owner detail. |
NIP-FI-CONF defines evidence packaging and mutation adequacy. Each uppercase
requirement above names the oracle that detects its violation.
## Security considerations
Delegation expands authority only by intersection and never by copying owner
capabilities. A stolen delegation still requires the delegate key. A stolen
delegate key is bounded by the relationship and finite lease. Owner rotation
cannot silently transfer delegation because the exact owner key and binding
version are dependencies. Implementations should invalidate by dependency index
rather than wait for incidental delegate traffic.

417
docs/nips/NIP-FI-EDGE.md Normal file
View File

@@ -0,0 +1,417 @@
# NIP-FI-EDGE: Trusted Edge Profile
`draft` `optional`
## Scope
This profile lets a trusted enterprise edge deliver federated assertion evidence to
a NIP-FI verifier. It defines two constructions:
- `trusted-proxy-hmac-v2`, a portable request-bound HMAC envelope; and
- a private authenticated-edge assertion adapter, for platforms that provide an
equivalent closed trust boundary without the stock envelope.
NIP-FI-EDGE is optional. A deployment can implement NIP-FI core using only
`client-attached`. Claiming this profile does not weaken core assertion validation,
independent Nostr proof, binding, lifecycle, policy, final-admission, or lease rules.
The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**,
**SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **NOT RECOMMENDED**, **MAY**, and
**OPTIONAL** are to be interpreted as described in BCP 14 when, and only when,
they appear in all capitals.
Every identifier this document serializes on the wire — header names, the
profile identifier, provenance envelope fields, and proof transport codes — is
interoperability-critical. `transport_contract_id` remains deployment-local as
core classifies it: its value is opaque outside a deployment, while the
canonical contract semantics this profile contributes to it are normative and
fixed here. Local adapter revision identifiers and key identifiers are
deployment-local and MUST NOT appear in public discovery.
## Common trusted-edge requirements
Server-owned listener, route, and authorization-domain configuration selects exactly
one edge profile before protected traffic is accepted. Request evidence cannot
select, negotiate, or downgrade that profile. Missing, repeated, comma-combined,
malformed, oversized, mixed-profile, or profile-inconsistent evidence denies without
fallback to `client-attached` or another edge profile.
Every trusted edge MUST:
1. strip every inbound copy of each `Nostr-Federated-Identity`,
`Nostr-Federated-Identity-Provenance`, and `Nostr-Federated-Identity-Client-Peer`
field, and of every other edge profile's assertion, identity, capability,
provenance, and client-peer field, before inserting its own fields. A trusted
edge MUST NOT remove or modify the `Authorization` field, which remains reserved
for the independent NIP-98 proof and MUST reach final admission unmodified;
2. cryptographically authenticate the immediate edge to the accepting origin and
isolate the origin from direct or alternate ingress;
3. integrity-protect every request component used by authorization other than
an independent Nostr proof, which is protected by its own signature;
4. apply a positive finite provenance deadline that is included in final admission
and every resulting lease;
5. validate a closed upstream identity and authorization claim set and produce the
same normalized assertion result required by core;
6. preserve the server-resolved domain, operation, resource, method, authority,
path/query, body semantics, proof transport, and Nostr actor key through final
admission; and
7. keep assertions, credentials, signatures, MACs, raw client addresses, and private
claims out of URLs, public protocol output, logs, metrics, and traces.
Header presence, source address, private-network location, hostname, or reachability
alone is not provenance. Accepting unsigned identity or capability headers, or
accepting signed headers without authenticating and isolating the immediate caller,
is nonconformant. A trusted edge that strips, rewrites, or reorders the
`Authorization` field, or that admits a proof-transport-`0x02` request whose
`Authorization` field did not arrive at the verifier byte-identical to the
client-sent value, is nonconformant.
An adapter's reviewed contract MUST identify its accepting origins, direct-origin
controls, field-stripping point, immediate-caller authentication, protected request
components, upstream assertion and policy validation, freshness bounds, independent
Nostr-proof path, compromise impact, and conformance evidence. It MUST deny when any
part of this boundary is absent or unreadable.
### Authenticated-edge assertion adapters
A deployment MAY install a private authenticated-edge adapter instead of HMAC-v2.
The adapter MUST satisfy all common requirements and demonstrate together:
origin isolation, cryptographically authenticated immediate caller, inbound-field
stripping, integrity of the complete authorization-relevant request, bounded
assertion and policy freshness, no direct-origin fallback, and the core final-
admission path with independent Nostr proof.
The adapter maps only its closed, validated claim set into the normalized result.
An opaque edge token is acceptable only inside this complete contract; opacity does
not make an unchecked header authoritative. Vendor names, issuer details, caller
identities, private field names, capability semantics, and adapter identifiers MUST
NOT appear in NIP-11 or portable examples.
## `trusted-proxy-hmac-v2`
The stock profile identifier is `trusted-proxy-hmac-v2`. Core computes the
`transport_contract_id` from a canonical contract that includes this profile's exact
wire format, protected components, replay rules, deadline rules, configured code
meanings, and adapter semantics. Changing any of those inputs produces a different
contract identity; the profile identifier itself remains stable. The proxy strips
all inbound assertion, provenance, and client-peer fields and inserts exactly one of
each:
```text
Nostr-Federated-Identity: Bearer <compact-JWS>
Nostr-Federated-Identity-Provenance: v2.<timestamp>.<nonce>.<mac>
Nostr-Federated-Identity-Client-Peer: <client-peer>
```
The assertion field follows core's compact-JWS and size rules. `timestamp` is
canonical unsigned decimal without leading zeroes, except zero is `0`. `nonce` and
`mac` are canonical unpadded base64url. Padding, the standard base64 alphabet,
ignored whitespace, or another encoding denies. The proxy generates a fresh nonce
containing at least 128 bits from a cryptographically secure random source. The
decoded MAC is exactly 32 octets. Finite field and decoded-nonce maxima are applied
before decoding, replay lookup, hashing, or allocation.
`client-peer` is at most 64 ASCII octets. IPv4 uses dotted decimal with no leading
zeroes. IPv6 uses lowercase RFC 5952 text. The edge converts an observed IPv4-mapped
IPv6 address to canonical IPv4 before constructing the field; a textual mapped IPv6
field is noncanonical. Empty, repeated, comma-combined, whitespace-padded, non-IP,
or noncanonical values deny. After verification, the verifier MAY retain only a
domain-separated keyed digest of this value in bounded private state.
The profile uses HMAC-SHA-256 with a deployment secret containing at least 256 bits.
Let `LP(x) = uint64be(len(x)) || x`, where length is in octets. The literal prefix is
14 ASCII octets and is not length-prefixed. The pre-MAC input is exactly:
```text
"NIP-FI-PROXY-2" ||
LP(timestamp_u64be) || LP(nonce_bytes) || LP(SHA256(jwt_ascii)) ||
LP(authorization_domain_id) ||
LP(method_ascii) || LP(authority_ascii) || LP(path_and_query_ascii) ||
LP(SHA256(payload_octets)) || LP(proof_transport_octet) || LP(client_peer_ascii)
```
`mac = HMAC-SHA-256(secret, pre_mac_input)`. The transmitted `mac` is canonical
unpadded base64url of the raw 32-octet result. The verifier compares it in constant
time.
### Canonical components
- **Timestamp:** Parse canonical decimal into an unsigned 64-bit integer, rejecting
overflow, then serialize it as exactly eight-byte big-endian. Freshness checks are
separate from serialization.
- **Nonce:** Decode the exact canonical base64url field before serialization.
- **Assertion:** Hash the exact ASCII compact-JWS octets after the one space in
`Bearer `. No whitespace, Unicode, JSON, or base64 normalization is allowed.
- **Authorization domain:** Configuration contains a canonical lowercase,
hyphenated RFC 9562 UUID named `authorization_domain_uuid`. Parse its 32 displayed
hexadecimal digits into the exact 16 UUID octets in display/network order. For
example, `00112233-4455-6677-8899-aabbccddeeff` becomes
`00112233445566778899aabbccddeeff`. UTF-8 UUID text, hashing, truncation,
namespace derivation, mixed-endian GUID encoding, uppercase, and unhyphenated
configuration are forbidden. The UUID is generated once, is immutable for the
domain's lifetime, and is shared through authenticated proxy/verifier
configuration. Duplicate UUIDs among active domains MUST fail startup.
- **Method:** Use the exact uppercase ASCII method token after trusted route
resolution. Lowercase or noncanonical input denies; the verifier does not repair it.
- **Authority:** Use server-configured lowercase ASCII host plus explicit effective
decimal port. IPv6 uses brackets and RFC 5952. Userinfo, a trailing dot, an omitted
port, percent encoding, or an authority derived solely from `Host`, `Forwarded`, or
`X-Forwarded-Host` denies.
- **Path and query:** Use the exact post-rewrite ASCII origin-form. Empty path becomes
`/`; a present query includes `?`. Percent octets and hex case, an empty query,
repeated names, and parameter order are preserved. No decoding, sorting,
dot-segment removal, or re-encoding may occur after the edge snapshot. An
unaccounted rewrite denies.
- **Payload:** Hash the complete HTTP payload octets after transfer-coding removal and
before content-coding decompression. These are exactly the octets forwarded by the
edge and exposed to verification. HTTP framing, chunk delimiters, and trailers are
excluded; `Content-Encoding` is not decoded. A WebSocket upgrade uses the empty
payload. Substitution of the protected octets after the snapshot denies.
- **Proof transport:** Serialize exactly one assigned octet from the registry below.
- **Client peer:** Serialize the exact canonical ASCII field value.
No authorization decision, target, resource, capability, or effect selector
derives from any request or connection component outside the protected pre-MAC
components, except an independent Nostr proof validated on its own signature,
such as the NIP-98 event in `Authorization` or the NIP-42 event after connect,
which the MAC does not protect; body interpretation follows the server-resolved
body semantics, never unprotected transport metadata such as `Content-Type` or
`Content-Encoding`.
### Freshness, replay, and key rotation
The deployment configures a positive finite `maximum_provenance_age` and a
non-negative finite `future_skew`. Evidence is live exactly when, using overflow-safe
comparisons:
```text
timestamp <= now + future_skew
now < timestamp + maximum_provenance_age
```
Equality at the age bound is expired. A direct lease deadline is no later than
`timestamp + maximum_provenance_age` and every core assertion, proof, policy, and
state deadline.
Absent, malformed, stale, future-dated, wrong-key, or mismatched provenance denies.
On a route that requires edge provenance, absent or incomplete provenance —
including provenance that omits the proxy-authenticated end-client peer — maps
to the `missing_evidence` public class, regardless of whether an assertion is
present. Provenance that is present and complete but fails verification maps to
`evidence_rejected`.
A v1 envelope denies. A verifier MAY try only a configured finite set of active
secrets. Rotation does not change nonce identity: replay uniqueness is scoped to
`(authorization_domain_id, trusted-proxy-hmac-v2, nonce)` and is independent of the
secret that verifies the MAC. A committed nonce is retained through at least
`timestamp + maximum_provenance_age`.
Preparation consumes neither nonce nor Nostr-proof replay identity. Final admission
atomically consumes both with any enrollment, receipt, and authorization decision.
A failed or rolled-back admission consumes neither. Two concurrent admissions with
the same nonce commit at most one authorization. The proxy-to-verifier hop still
requires confidentiality and integrity.
## Proof-transport code registry
| Code | Meaning and allocation policy |
|---|---|
| `0x00` | Invalid; MUST deny. |
| `0x01` | NIP-42 connection proof. |
| `0x02` | NIP-98 HTTP proof. |
| `0x03` | Git smart-HTTP session proof: the proxy verifies a session-scoped Nostr authorization for a Git smart-HTTP request before forwarding. Reserved; allocation completes on publication of its transport contract (see below). |
| `0x04` | Blossom media proof: the proxy verifies a Blossom media-HTTP authorization event for the request before forwarding. Reserved; allocation completes on publication of its transport contract (see below). |
| `0x05`–`0x7f` | Unassigned; allocation requires a published stable specification. |
| `0x80`–`0xfe` | Private use under an explicit shared proxy/verifier contract only. |
| `0xff` | Reserved for a future extended encoding; invalid in HMAC-v2. |
An allocation MUST define exact proof validation, request binding, freshness, replay
identity and window, and conformance vectors. Assigned semantics never change; an
incompatible meaning receives a new code. Unknown, unconfigured, or private-use
codes without the same configured contract at proxy and verifier deny. Private-use
codes MUST NOT be advertised as portable NIP-FI-EDGE interoperability.
Codes `0x03` and `0x04` are reserved to fix their meanings and prevent
reassignment; their transport contracts are not yet published, so their
allocations are not complete. Until the contract for such a code is published,
the code is valid only under an explicit shared proxy/verifier contract,
exactly as for private use, and MUST NOT be presented as portable NIP-FI-EDGE
interoperability.
## Bounded payload acquisition
Every protected `(authorization_domain_id, route, proof_transport_code)` tuple MUST
configure a finite `maximum_payload_octets` and finite per-request
`maximum_spool_octets >= maximum_payload_octets`. Zero is allowed only for a route
that requires an empty payload. Proxy and verifier configuration MUST agree and is
part of the transport contract.
If trusted `Content-Length` exceeds the route limit, the edge denies before reading,
hashing, JWT verification, replay lookup, or authoritative mutation. For absent,
unknown, or streamed length, acquisition uses a bounded counter and spool and stops
on octet `limit + 1`. Incremental SHA-256 is allowed, but no digest or prefix can
authorize until EOF proves completeness.
Spooling uses memory or access-controlled temporary storage with finite per-request
and aggregate quotas, cleanup on every outcome, no public or log output, and no reuse
across requests. Quota exhaustion fails closed and creates no nonce claim, proof
claim, receipt, lease, or application mutation. At or below the limit, the exact
captured payload is replayed unchanged. HMAC verification and core final admission
complete before application effects. Forwarding to a rollback-safe private spool is
not an application effect; forwarding to a parser, decoder, handler, or origin that
can act is.
A content decoder, multipart parser, Git/Blossom handler, framework, or intermediary
that cannot expose and replay the exact stage defined above before effects cannot
claim HMAC-v2 for that route. It MUST use core `client-attached` or another specified
edge profile, never a partial-body MAC.
## Normative HMAC-v2 vectors
All vector integers and lengths are big-endian. Common values are:
```text
secret_hex = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
nonce_hex = 000102030405060708090a0b0c0d0e0f
nonce_base64url = AAECAwQFBgcICQoLDA0ODw
authorization_domain_uuid = 00112233-4455-6677-8899-aabbccddeeff
authorization_domain_id_hex = 00112233445566778899aabbccddeeff
jwt_ascii = eyJhbGciOiJFUzI1NiIsInR5cCI6Im5pcC1maStqd3QifQ.eyJpc3MiOiJodHRwczovL2lkLmV4YW1wbGUiLCJzdWIiOiIxMjMifQ.c2ln
assertion_digest_hex = 6103b52a52730bc065d65673247603a63c9810488c90d0ada3d8d227eee5285f
```
The fixture JWT represents a separately minted `nip-fi+jwt` assertion and is opaque
test input; its deliberately synthetic signature is not an assertion-validation
vector. Implementations MUST reproduce each field, complete pre-MAC input,
diagnostic input digest, raw MAC, and wire MAC exactly
(`FI-TRACE-EDGE-VECTORS`).
### Vector 1: HTTP / NIP-98 / non-empty payload
```text
timestamp_decimal = 1700000000
timestamp_u64be_hex = 000000006553f100
method_ascii = POST
authority_ascii = api.example:443
path_and_query_ascii = /upload?part=1&part=2&x=%2F
payload_hex = 68656c6c6f0a
body_digest_hex = 5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03
proof_transport_hex = 02
client_peer_ascii = 203.0.113.9
pre_mac_input_hex = 4e49502d46492d50524f58592d320000000000000008000000006553f1000000000000000010000102030405060708090a0b0c0d0e0f00000000000000206103b52a52730bc065d65673247603a63c9810488c90d0ada3d8d227eee5285f000000000000001000112233445566778899aabbccddeeff0000000000000004504f5354000000000000000f6170692e6578616d706c653a343433000000000000001b2f75706c6f61643f706172743d3126706172743d3226783d25324600000000000000205891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03000000000000000102000000000000000b3230332e302e3131332e39
pre_mac_input_sha256 = df2870230d2170595dccd17d9e61a82282d8cd8b978ac18bff07419ed59091d5
mac_hex = 761d3ecbf609f0f558b4a02a1a18a25070f3dbe89fce9cac59a80bce4436ade5
mac_base64url = dh0-y_YJ8PVYtKAqGhiiUHDz2-ifzpysWagLzkQ2reU
provenance = v2.1700000000.AAECAwQFBgcICQoLDA0ODw.dh0-y_YJ8PVYtKAqGhiiUHDz2-ifzpysWagLzkQ2reU
```
### Vector 2: WebSocket / NIP-42 / empty payload / mapped peer
The edge observed `::ffff:192.0.2.128` and emitted canonical `192.0.2.128`.
```text
timestamp_decimal = 1
timestamp_u64be_hex = 0000000000000001
method_ascii = GET
authority_ascii = relay.example:443
path_and_query_ascii = /
payload_hex =
body_digest_hex = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
proof_transport_hex = 01
client_peer_ascii = 192.0.2.128
pre_mac_input_hex = 4e49502d46492d50524f58592d32000000000000000800000000000000010000000000000010000102030405060708090a0b0c0d0e0f00000000000000206103b52a52730bc065d65673247603a63c9810488c90d0ada3d8d227eee5285f000000000000001000112233445566778899aabbccddeeff0000000000000003474554000000000000001172656c61792e6578616d706c653a34343300000000000000012f0000000000000020e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855000000000000000101000000000000000b3139322e302e322e313238
pre_mac_input_sha256 = 67564d241499491b3ea53b31d6111fbc9efac37a294f6ce591519e4bf21b53e9
mac_hex = f71a179a018637a0582cf3de39ccb7b976216c18ada312127d4c983c14af4b20
mac_base64url = 9xoXmgGGN6BYLPPeOcy3uXYhbBitoxISfUyYPBSvSyA
```
### Vector 3: IPv6 authority and path/query byte preservation
```text
timestamp_decimal = 1700000000
timestamp_u64be_hex = 000000006553f100
method_ascii = GET
authority_ascii = [2001:db8::1]:443
path_and_query_ascii = /a%2Fb?b=2&a=1&a=0
payload_hex =
body_digest_hex = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
proof_transport_hex = 02
client_peer_ascii = 2001:db8::2
pre_mac_input_hex = 4e49502d46492d50524f58592d320000000000000008000000006553f1000000000000000010000102030405060708090a0b0c0d0e0f00000000000000206103b52a52730bc065d65673247603a63c9810488c90d0ada3d8d227eee5285f000000000000001000112233445566778899aabbccddeeff000000000000000347455400000000000000115b323030313a6462383a3a315d3a34343300000000000000122f61253246623f623d3226613d3126613d300000000000000020e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855000000000000000102000000000000000b323030313a6462383a3a32
pre_mac_input_sha256 = 8a93a29c4ac30b0f2551d346d0636040b639bb1f109d287e93ce44ddaed73e33
mac_hex = df2936f81d752f3d6bac2a36d3381c38db2c9abc3570236cb121274ad34a6161
mac_base64url = 3yk2-B11Lz1rrCo20zgcONssmrw1cCNssSEnStNKYWE
```
### Serialization and negative matrix
The following timestamp values MUST serialize as shown before freshness evaluation:
| Decimal | `uint64be` hex |
|---:|---|
| `0` | `0000000000000000` |
| `1` | `0000000000000001` |
| `255` | `00000000000000ff` |
| `256` | `0000000000000100` |
| `18446744073709551615` | `ffffffffffffffff` |
`00`, `01`, `+1`, surrounding whitespace, negative values, and
`18446744073709551616` deny before MAC comparison. The maximum value above is an
encoding vector; ordinary freshness policy will reject it.
Every implementation MUST run these normative negative cases:
| Class | Required cases and result |
|---|---|
| Envelope | Absent/repeated/comma-combined fields, `v1`, missing/extra component, padding, alternate alphabet, nonce below 16 octets or above configured max, and MAC lengths 31 or 33 all deny. |
| Domain | Uppercase/nonhyphenated UUID config fails configuration; mixed-endian UUID bytes or any one-bit domain transplant fails the baseline MAC; duplicate active UUID fails startup. |
| Request | Mutating assertion, method, authority, path/query, body, proof code, or peer while retaining Vector 1's MAC denies. |
| Metadata | Mutating `Content-Type` or `Content-Encoding` in flight changes no authorization decision, target, capability, or effect selector; a request whose server-resolved body semantics no longer hold denies. |
| Path | `%2F`→`%2f`, decoding to `/`, reordering repeated query values, or adding/removing an empty `?` fails the baseline MAC. |
| Authority | Unbracketed or non-RFC-5952 IPv6, uppercase host, trailing dot, or missing port denies before MAC comparison. |
| Peer | Textual `::ffff:192.0.2.128`, padded IPv4, uppercase/noncanonical IPv6, or whitespace denies before MAC comparison. |
| Proof | `0x00`, `0xff`, unknown stock code, or private code without a shared configured contract denies. |
| Body | Known and unknown lengths `0`, `limit-1`, and `limit` may proceed only after EOF; `limit+1`, disconnect before EOF, aggregate-quota exhaustion, or any post-snapshot substitution of the protected octets denies with no replay or authoritative mutation. |
| Replay | Concurrent final admissions of one valid envelope commit at most one; preparation and failed final admission consume none; secret rotation does not create a new nonce namespace. |
| Fallback | Direct ingress, mixed evidence, and failed HMAC never retry as `client-attached` or another adapter. |
## Discovery and conformance
A relay that completely implements the stock profile MAY add exactly
`"edge_transports": ["trusted-proxy-hmac-v2"]` inside the top-level NIP-11
`federated_identity` object. `edge_transports` is an array of unique ASCII string
profile identifiers in ascending bytewise order; this document assigns only the
single value shown. A relay that does not completely implement the stock profile
MUST omit the member. It MUST NOT advertise private adapters, keys, domains, field
names, or code contracts. No request may select behavior from this discovery
member; server-owned configuration selects the edge profile. Claiming FI-EDGE
requires every configured edge profile to pass the applicable core conformance suite
and these profile traces:
| Trace | Required oracle |
|---|---|
| `FI-TRACE-EDGE-VECTORS` | Reproduce all three normative vectors field-for-field, including each complete pre-MAC input, diagnostic input digest, raw MAC, and wire MAC; reproduce all five timestamp serialization rows; every listed serialization and negative-matrix case produces its required denial or configuration failure. |
| `FI-TRACE-PROXY-SPOOF` | Direct ingress, unsigned/header-only identity, unauthenticated caller, or invalid provenance denies without fallback. |
| `FI-TRACE-PROXY-REPLAY` | Two HMAC-v2 final admissions using one nonce commit at most one; preparation consumes neither. A private adapter proves its declared replay semantics. |
| `FI-TRACE-PROXY-CROSS-REQUEST` | Each protected component mutation denies. HMAC-v2 covers assertion, domain, method, authority, path/query, complete body, proof transport, and peer. On a `0x02` route the `Authorization` bytes at final admission equal the client-sent bytes, witnessed at both points; an edge that substitutes a valid proof from the same actor fails the witness. |
| `FI-TRACE-EDGE-BODY-BOUNDS` | Known and streamed boundary cases prove bounded work/storage, EOF completeness, cleanup, and no pre-authorization effect. |
| `FI-TRACE-EDGE-KEY-ROTATION` | A finite active-key set accepts an intended overlap without allowing nonce reuse or an unknown key. |
The conformance record binds the exact implementation, adapter, deployment,
assertion policy, transport contract, configured code meanings, and vector revision.
Two HMAC-v2 implementations interoperate only when they reproduce all valid vector
bytes exactly, reject every negative, agree on UUID and code configuration, and
preserve atomic replay and bounded complete-body behavior.
## Security considerations
HMAC-v2 limits header spoofing, replay, and cross-request transplantation only when
its secret remains confidential, the edge snapshots the final routed request, the
origin authenticates that edge, and final admission atomically consumes replay state.
It does not replace TLS or independent Nostr proof. A compromised edge or shared
secret can forge federated evidence within its configured domains; use distinct
secrets and UUIDs to limit blast radius.
Authenticated-edge adapters intentionally shift more proof to deployment controls.
A hostname, private network, or opaque token is not an equivalent construction unless
the complete boundary obligations above are demonstrated. Body buffering and replay
state are attacker-controlled resource surfaces, so all field, payload, spool,
aggregate, key-set, and retention bounds fail closed.

View File

@@ -0,0 +1,269 @@
# NIP-FI-LIFECYCLE: Binding Lifecycle Profile
`draft` `optional`
## Abstract
This profile extends NIP-FI with provisioned enrollment, identity disablement,
re-enablement, and an administrative binding-expiry gate. It is for deployments
whose binding changes require separately authorized operator or enterprise
workflows. It does not change NIP-FI assertion validation, Nostr proof, final
admission, or public denial semantics.
The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**,
**SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this
document are to be interpreted as described in BCP 14 when, and only when, they
appear in all capitals as shown here.
## Dependencies and claim
An implementation of this profile implements NIP-FI Core and advertises only
the boolean `"lifecycle": true` inside its NIP-11 `federated_identity` object.
This boolean claims support for this profile; it deliberately reveals neither an
enrollment mode nor lifecycle state. For a fixed set of claimed profiles, the
complete discovery output MUST be byte-identical whether enrollment is
attested-key, TOFU, or provisioned and whether lifecycle facts exist. A server
MUST NOT advertise the claim until every protected ingress in the advertised
authorization domain applies this profile through the same final-admission
authority (`FI-LC-CLAIM`).
This profile contributes lifecycle dependencies and deadlines to the core
prepared decision and lease. They compose with core dependencies by set union;
the earliest applicable deadline wins. This profile cannot weaken, replace, or
bypass a core check.
## Additional state
For authorization domain `D`, this profile adds:
```text
X_D : set of disabled identities
Q_D : identity -> pending lineage
PendingLineage = (
identity,
old_key,
old_binding_version
)
```
It also permits a core binding to carry `binding_not_after`, an optional
administrative deadline. The pending lineage names one exact retired pair and
binding version. There is at most one pending lineage per identity.
`binding_not_after` bounds the grant represented by **one binding**; it is not a
bound on the identity. Rotation continues the same grant: the replacement binding
preserves the carried bound, and only expiry authority changes it. Retirement,
revocation, and disablement end the existing grant. Re-enablement, provisioning,
and ordinary enrollment establish a *new* grant that carries no prior bound — so
retirement of a bound pair followed by ordinary enrollment under `attested-key`
or `tofu` policy yields an unbounded binding, and that is conformant. A deadline that must survive the end
of a grant — an identity-scoped access bound — belongs in the capability
projection of authoritative local-policy state, which core already requires for
any projection whose removal must close authority within a declared bound and
which is reread at preparation, final admission, and every protected lease use.
`binding_not_after` is not that mechanism and cannot substitute for it: it does
not survive the end of the grant that carries it, so a deployment relying on it
for identity-scoped expiry cannot claim a revocation bound for that expiry
(`FI-LC-ADMIN-EXPIRY`).
A binding carrying a reached bound is **active** for every core relation and
eligibility test in this profile and in core, including the core partial
bijection and `TargetEligible` below. It is ineligible for authorization, not
absent from the binding relation.
`X_D`, `Q_D`, and `binding_not_after` are deployment-local state. Their versions
are revalidation dependencies, not contract identities. A change invalidates a
prepared decision and every dependent lease unless complete final-admission
recomputation produces the required current result.
Ordinary authorization MUST deny when its identity is disabled, when pending
lineage exists for that identity, or when `now >= binding_not_after`; it MUST
NOT clear, consume, or alter any of those facts (`FI-LC-ORDINARY-GATES`). An
absent `binding_not_after` has no administrative expiry. Assertion `exp`,
`iat`, refresh, or maximum age never creates, renews, extends, or clears it.
Time passage alone creates no tombstone, lineage, or history.
## Private denial conditions
This profile defines exactly these private condition identifiers and owning
public classes for NIP-FI-CONF enumeration agreement:
| Private condition identifier | Public class |
|---|---|
| `identity_disabled` | `authorization_denied` |
| `explicit_replacement_required` | `authorization_denied` |
| `binding_expired` | `authorization_denied` |
The identifiers are fixture names, not wire values. Adding, removing, renaming,
or reclassifying one requires the same change in NIP-FI-CONF's denial-fixture
table.
## Common transition contract
Each transition below requires privileged authority distinct from an ordinary
federated assertion and Nostr proof. That authority MUST be bound to the exact
`D`, transition name, identity, request, old binding version when present, and
target key when present (`FI-LC-AUTHORITY`). The deployment defines how that
authority is obtained; role names, approval count, and operator APIs are out of
scope.
A transition MUST, in one atomic commit:
1. validate that privileged authority and fresh target-key evidence;
2. read and recheck the applicable core binding relation, retired pairs,
revoked keys, `X_D`, `Q_D`, policy, and dependency versions;
3. apply exactly the state changes specified below;
4. append immutable lifecycle history identifying the transition and versions;
and
5. advance lifecycle state so dependent prepared decisions and leases cannot
authorize after commit.
A stale precondition, denied transition, unreadable dependency, or failed commit
MUST leave all authoritative state unchanged (`FI-LC-ATOMIC`). Lease
invalidation MAY be delivered asynchronously, but authorization use after the
commit MUST recheck the advanced dependency before allowing an operation.
Every transition that creates a binding MUST state whether it **continues** an
existing grant, and therefore preserves that grant's administrative bound, or
**establishes** a new grant carrying no prior bound. The two cases partition the
binding-creating transitions with no remainder: core rotation continues, and
provisioning, re-enablement, and ordinary enrollment establish. A profile that
adds a binding-creating transition without this declaration cannot claim
conformance (`FI-LC-CLAIM`).
`TargetEligible(i, k, allow_disabled)` means that `k` is not revoked, `(i, k)`
is not retired, neither `i` nor `k` has an active binding, and `i` is not
disabled unless `allow_disabled` is true. Every new target key requires fresh,
request-bound Nostr proof by that key. If domain policy requires issuer key
attestation, the transition also requires a current assertion for `i` whose key
claim equals `k`. Supplied stale, absent, wrong-identity, or mismatched required
attestation denies; it is never ignored as optional evidence
(`FI-LC-TARGET-PROOF`).
A replacement binding records `attested-key` provenance only when current
matching issuer attestation was validated; otherwise it records `provisioned`.
TOFU provenance can arise only from the core ordinary first-use extension and
is never inherited by a replacement.
## Privileged transitions
### Provision binding
```text
ProvisionBinding(i, k):
require domain enrollment policy = provisioned
require TargetEligible(i, k, false)
require Q_D(i) is absent
require fresh target-key evidence
create Binding(i, k, new_version, provisioned)
```
The transition creates no authorization lease. Later use requires a current
assertion, fresh Nostr proof, and ordinary final admission. Ordinary
request-time authorization under `provisioned` policy MUST NOT create a binding
(`FI-LC-PROVISION`).
### Disable identity
```text
DisableIdentity(i):
add i to X_D
if Binding(i, k, old_version) exists:
remove Binding(i, k, old_version)
add (i, k) to the core retired-pair set
set Q_D(i) = (i, k, old_version)
```
Applying an authorized disablement repeatedly is idempotent. It MUST NOT erase
or replace existing lineage. If `i` has no active binding, disablement creates
no lineage (`FI-LC-DISABLE`).
### Re-enable identity
```text
ReenableIdentity(i, expected_lineage?, k_new):
require i is in X_D
require Q_D(i) is absent when expected_lineage is absent,
otherwise require Q_D(i) = expected_lineage
require TargetEligible(i, k_new, true)
require fresh target-key evidence
remove i from X_D
consume expected_lineage when present
create Binding(i, k_new, new_version, ReplacementProvenance(evidence))
```
Clearing disabled state and creating the target binding are inseparable. There
is no clear-only transition: it would permit a later ordinary enrollment to
capture the identity. An operator that intends to provision later leaves the
identity disabled until the target and fresh proof are available
(`FI-LC-REENABLE`).
### Set administrative expiry
```text
SetAdministrativeExpiry(i, k, old_version, binding_not_after?):
require exact current Binding(i, k, old_version)
require separate privileged expiry authority
replace it with Binding(i, k, new_version,
same_provenance, binding_not_after?)
```
This transition changes neither side of the pair nor its provenance. Setting,
replacing, or clearing the bound advances the binding version. At equality the
binding is ineligible but remains durable and occupies both sides of the core
partial bijection. This transition is the only expiry authority: no other
transition in this profile or in core sets, replaces, or clears the bound, and
core rotation carries it onto the replacement binding unchanged. Only this or
another applicable privileged transition can restore access; ordinary
authorization cannot renew the bound (`FI-LC-ADMIN-EXPIRY`).
## One-shot lineage and concurrency
Consumption of `Q_D` and creation of its replacement binding MUST be one
compare-and-commit operation over the exact pending lineage. Of two concurrent
re-enablings presenting the same lineage, at most one can commit. The loser
observes changed state and denies without creating a binding, consuming another
lineage, or changing history (`FI-LC-QD-ONCE`).
A lifecycle transition racing ordinary final admission is ordered by the same
authoritative state transaction or dependency check. If the lifecycle commit
wins, the ordinary operation denies; if final admission wins first, the
lifecycle transition still invalidates subsequent lease use. No ordering
permits authority from a disabled identity, consumed lineage, or expired
binding after the corresponding state change is observed.
## Behavioral oracles
Each oracle is normative. A conforming implementation produces the stated
result at final admission and retains no partial authoritative mutation from a
denied case.
| ID | Setup and required result |
|---|---|
| `FI-LC-CLAIM` | For a fixed profile set, compare complete discovery bytes across attested-key, TOFU, and provisioned configurations and across lifecycle states: they are identical. If one protected ingress omits lifecycle gates or uses a different lifecycle lineage, the domain cannot advertise the profile and the uncovered ingress fails closed. If any claimed profile defines a binding-creating transition that declares neither grant continuation nor grant establishment, the domain cannot advertise that profile. |
| `FI-LC-ORDINARY-GATES` | Fresh assertion and proof for a disabled identity, an identity with pending lineage, and a binding at administrative-expiry equality each deny without changing lifecycle state. |
| `FI-LC-AUTHORITY` | An ordinary assertion plus valid Nostr proof, but no transition-specific authority, cannot perform any transition; mutation of any authority-bound field denies. |
| `FI-LC-ATOMIC` | Inject failure at each transition write boundary; no binding, tombstone, disabled fact, lineage, history entry, or dependency version is partially committed. |
| `FI-LC-TARGET-PROOF` | Missing, stale, wrong-key, wrong-request, or mismatched required attestation for a new target denies without mutation. |
| `FI-LC-PROVISION` | Ordinary first use in provisioned mode denies; authorized provisioning creates one binding and no lease; later current ordinary admission may use it. |
| `FI-LC-DISABLE` | Disabling an active identity atomically disables it, retires its exact pair, records exact lineage, and closes subsequent lease use; replay is idempotent and preserves lineage. |
| `FI-LC-REENABLE` | Re-enablement creates an eligible proven binding in the same commit that clears disabled state; absent or wrong expected lineage and a clear-only attempt deny. |
| `FI-LC-ADMIN-EXPIRY` | Before the bound the binding may authorize; at equality it denies while still occupying the relation, so a target eligibility test for either side of that pair fails. Rotating the expired binding to a new key carries the bound: the replacement denies at the same instant. No non-expiry transition clears it. Only an authorized version-checked update by the expiry authority changes the bound. Conversely, retirement, revocation, or re-enablement of the bound pair followed by an authorized or ordinary new grant produces an unbounded binding, which is the required result and not an escape. |
| `FI-LC-QD-ONCE` | Two concurrent re-enablings consume the same `Q_D` lineage; exactly one commits and the loser leaves every authoritative store unchanged. |
| `FI-LC-RACE` | Race each transition against prepared ordinary admission and lease use; no operation authorizes after observing the advanced lifecycle or binding dependency. |
## Security considerations
Privileged authority compromise can provision or replace enterprise bindings;
deployments should apply controls proportionate to that authority. This profile
makes the authority request-bound and transitions atomic, but does not define
approval UX or key custody.
Disabled identities, retired pairs, revoked keys, and pending lineage serve
different purposes. Re-enablement removes only the exact disabled fact and
optional exact lineage named by its transition. No transition in this profile
removes a core revoked-key or retired-pair fact.
Administrative expiry is local policy, not upstream revocation freshness. It
cannot extend an assertion, status witness, Nostr proof, or lease deadline.

96
docs/nips/NIP-FI-MODEL.md Normal file
View File

@@ -0,0 +1,96 @@
NIP-FI-MODEL
============
Composed authorization model (non-normative)
--------------------------------------------
This companion is explanatory. It defines no requirement, invariant, wire
value, denial mapping, or conformance claim. Normative requirements live in
[NIP-FI](NIP-FI.md) and the claimed profile documents. In particular,
`FI-INV-01` through `FI-INV-16` are defined only by NIP-FI core.
## State sketch
One useful implementation model keeps these authoritative relations per domain:
```text
B_D : active identity-to-key relation
T_D : retired identity/key pairs
Y_D : revoked keys
H_D : immutable lifecycle history
V_D : binding and lifecycle versions
```
NIP-FI-LIFECYCLE adds disabled identities and pending replacement lineage.
NIP-FI-DELEG adds relationship state but no delegate binding. NIP-FI-EDGE adds
transport-provenance and replay witnesses. Implementations may use different
storage as long as their observable behavior satisfies the owning normative
documents.
## Composed direct decision
The core decision can be read as this equation:
```text
validated issuer-qualified identity
+ fresh request/connection-bound Nostr proof
+ current durable partial-bijection state
+ current local policy
+ atomic final admission
= authority for exactly the proven key and operation
```
Preparation gathers immutable evidence and snapshots every dependency without
mutation. Final admission compares exact context and stable contract identities,
checks all deadlines, revalidates changed snapshots, recomputes from current
binding and policy state, then commits replay claims, optional enrollment, and a
receipt atomically. The special concurrent-enrollment normalization is narrow:
an `enroll(i,k)` proposal may become the same eligible `existing(i,k)` result;
a different winner is not equivalent.
## Profile composition
Profiles contribute witnesses, never alternate final authority:
```text
core witnesses
∪ EDGE provenance/replay witnesses
∪ LIFECYCLE eligibility/lineage witnesses
∪ DELEG owner/relationship witnesses
```
The lease deadline is the minimum of every bound in the resulting set. A missing
or unreadable required witness denies. A profile cannot remove a core witness,
extend a core deadline, replace the proven actor, or create a second admission
lineage.
For direct authorization, the path dependency is the normalized assertion and
its current snapshot/status witnesses. For delegated authorization, it is the
exact eligible owner binding plus relationship evidence; direct assertion fields
are absent. Both paths share context resolution, Nostr-proof validation, local
policy, read-only preparation, and atomic final admission.
## Lifecycle intuition
Bindings are durable; leases are ephemeral. Retirement makes one exact pair
permanently ineligible for ordinary recreation. Revocation makes a key
ineligible throughout the domain. Rotation retires the old pair and creates a
new binding version but does not globally revoke the old key. Extended lifecycle
operations may add disabled identity and one-shot pending-lineage state, as
specified by NIP-FI-LIFECYCLE.
## Privacy intuition
Private reasons collapse to fixed public bytes. In particular, binding
conflicts, tombstones, lifecycle gates, key mismatch, enrollment requirements,
and local-policy decisions are indistinguishable. Operational diagnostics may
retain bounded private reason codes, but such records are not protocol objects
and never become authorization witnesses.
## Reading order
1. NIP-FI for core state, wire behavior, invariants, and direct admission.
2. NIP-FI-EDGE for a trusted-enterprise edge.
3. NIP-FI-LIFECYCLE for provisioning, disablement, and re-enablement.
4. NIP-FI-DELEG for delegated agents.
5. NIP-FI-CONF for claim and evidence rules.

632
docs/nips/NIP-FI.md Normal file
View File

@@ -0,0 +1,632 @@
NIP-FI
======
Federated identity authorization — core
----------------------------------------
`draft` `optional` `relay`
**Protocol dependencies**: NIP-01 and either NIP-42 or NIP-98. Optional
profiles are defined by NIP-FI-EDGE, NIP-FI-LIFECYCLE, NIP-FI-DELEG, and
NIP-FI-CONF.
The key words "MUST", "MUST NOT", "REQUIRED", "SHOULD", "SHOULD NOT", and
"MAY" in this document are to be interpreted as described in BCP 14 (RFC 2119
and RFC 8174) when, and only when, they appear in all capitals.
## Abstract
NIP-FI authorizes a Nostr key only when four independent facts agree: a valid
issuer-qualified identity assertion, fresh proof of that Nostr key, current
identity-to-key binding state, and current local policy for the exact operation.
The identity provider never signs Nostr events, and an assertion never replaces
Nostr proof.
Bindings outlive individual assertions. Assertions and authorization leases do
not outlive their evidence. This core defines the portable client-attached
assertion transport, direct enrollment, atomic final admission, bounded
sessions, privacy-preserving denial responses, and the smallest useful binding
lifecycle. Companion profiles add trusted edges, extended lifecycle operations,
and delegation without changing the core admission rule.
This NIP does not define an identity provider, database schema, operator API,
public identity projection, application membership policy, or user interface.
## Terms and identifier classes
- **domain** (`D`): an authorization boundary selected only by authenticated
server routing and configuration.
- **identity** (`i`): the exact tuple `(iss, sub)` returned by assertion
validation. Email, display name, employee number, and a bare `sub` are not
identities.
- **target context** (`R_t`): the server-resolved method, authority, path and
query, body semantics, transport, operation, and resource.
- **actor** (`k`): the 32-byte public key returned by Nostr-proof validation.
- **request context** (`R`): `R_t` sealed with `k`.
- **binding**: a durable, versioned association `(D, i, k)` with immutable
provenance `attested-key`, `tofu`, or `provisioned`.
- **retired pair**: a durable denial fact for an exact `(D, i, k)`.
- **revoked key**: a durable denial fact for `(D, k)`.
- **prepared authorization**: immutable, read-only evidence and witnesses for a
possible admission.
- **committed authorization**: authority returned only after final revalidation
and atomic commit.
- **lease**: a cached committed decision for one actor and bounded operation
set. A lease is not a binding.
Identity and authorization-state comparisons preserve every tuple component.
Equal `sub` values under different `iss` values are distinct identities; equal
`(i, k)` pairs under different domains are distinct bindings, retired pairs,
and authorization state. [FI-TRACE-CROSS-DOMAIN-COLLISION]
Every identifier is either **interoperability-critical** or
**deployment-local**. Header names, public response bytes, token type values,
and trace identifiers are interoperability-critical and fixed here.
`assertion_policy_id`, `transport_contract_id`, domain IDs, snapshot versions,
binding versions, policy versions, and correlation IDs are deployment-local;
their values are opaque outside a deployment, while their stability and
invalidation behavior are normative.
## Core security invariants
These labels are the normative home of the NIP-FI invariants. Companion
profiles may add witnesses and bounds but cannot weaken them.
1. **`FI-INV-01 — partial bijection.`** Active bindings are one-to-one within a
domain: one identity has at most one active key and one key has at most one
active identity. [FI-TRACE-BINDING-CONFLICT]
2. **`FI-INV-02 — durable binding.`** Assertion expiry removes neither a
binding nor its provenance. Fresh eligible evidence may authorize the same
binding later. [FI-TRACE-ASSERTION-REFRESH]
3. **`FI-INV-03 — tombstone monotonicity.`** Ordinary authorization never
removes a retired-pair or revoked-key fact and never recreates a retired
pair. [FI-TRACE-TOMBSTONE-REPLAY]
4. **`FI-INV-04 — server-owned context.`** Every admitted operation uses one
server-resolved domain, target, resource, operation, and proven actor.
Unauthenticated input cannot replace them. [FI-TRACE-DOMAIN-SPOOF]
5. **`FI-INV-05 — independent evidence.`** Direct authorization requires a
current assertion and fresh Nostr proof. If the assertion names a key, it
equals the proven actor. [FI-TRACE-ASSERTION-KEY-MISMATCH]
6. **`FI-INV-06 — stable assertion policy.`** Assertion-policy identity changes
when accepted assertion semantics change, but not when only authenticated
key or status snapshot contents rotate. [FI-TRACE-VERIFIER-PARITY]
7. **`FI-INV-07 — current-snapshot verification.`** Evidence cannot survive
removal of the key or policy snapshot that authenticated it; a changed
snapshot requires revalidation. [FI-TRACE-JWKS-REMOVE]
8. **`FI-INV-08 — read-only preparation.`** Preparation creates no binding,
tombstone, replay claim, receipt, lease, publication, last-seen value, audit
authority, or application mutation. [FI-TRACE-FINAL-DENIAL-NO-MUTATION]
9. **`FI-INV-09 — atomic final admission.`** Enrollment, replay claims,
receipts, and required authorization evidence commit only after complete
final revalidation, all or none. [FI-TRACE-PREPARED-STALE]
10. **`FI-INV-10 — explicit lifecycle authority.`** Retirement, revocation,
rotation, and profile-defined lifecycle changes occur only through their
separately authorized transition. [FI-TRACE-LIFECYCLE-AUTHORITY]
11. **`FI-INV-11 — evidence-bounded leases.`** A lease ends no later than every
evidence, snapshot, proof, binding, local-policy, and implementation bound
on which it depends. [FI-TRACE-LEASE-BOUND]
12. **`FI-INV-12 — current-owner delegation.`** When NIP-FI-DELEG is claimed,
delegation requires the exact current eligible owner binding, fresh
delegate proof, capability intersection, and a positive finite deadline.
[FI-DELEG-OWNER-CURRENT]
13. **`FI-INV-13 — privacy-safe denial.`** Public rejection is many-to-one and
reveals no identity, key, claim, binding, tombstone, enrollment mode, key
identifier, or private policy fact. [FI-TRACE-DENIAL-ORACLE]
14. **`FI-INV-14 — fail closed.`** Unreadable, ambiguous, stale beyond policy,
or inconsistent evidence or authoritative state cannot produce authority.
[FI-TRACE-DEPENDENCY-FAIL-CLOSED]
15. **`FI-INV-15 — uniform authority.`** Every protected ingress in a domain
uses the same current domain policy and final-admission authority. An
uncovered or competing path is unavailable. [FI-TRACE-AUTHORITY-UNIFORM]
16. **`FI-INV-16 — canonical verifier.`** Assertion transports feed one closed,
provider-neutral normalized-result contract and cannot fork final
admission. [FI-TRACE-VERIFIER-PARITY]
## Client-attached transport
Server configuration selects `client-attached` before protected traffic is
accepted. Request fields cannot select, negotiate, or downgrade transport.
Failure never falls back to another transport. [FI-TRACE-TRANSPORT-CLOSED]
The client sends exactly one field on the request or WebSocket upgrade:
```text
Nostr-Federated-Identity: Bearer <compact-JWS>
```
`Authorization` remains reserved for NIP-98. Assertion and provenance fields
from any other profile are absent. Missing, repeated, comma-combined, empty,
malformed, non-Bearer, or mixed-profile fields deny. Assertions never appear in
URLs, query parameters, Nostr events, tags, filters, application history, or
public identity projections. [FI-TRACE-TRANSPORT-CLOSED]
The core transport contract has deployment-local identity
`transport_contract_id`. It deterministically identifies the exact field,
parsing, request-attachment, no-fallback, and context-preservation semantics.
Changing any of those semantics changes the ID; changing request data does not.
[FI-TRACE-CONTRACT-IDENTITIES]
## Assertion validation
A configured assertion policy accepts exactly one bounded compact JWS and
returns this closed result:
```text
VerifiedAssertion = (
identity = (iss, sub),
asserted_key?,
claims_or_capabilities,
authority_deadlines, // non-empty
assertion_policy_id,
transport_contract_id,
revalidation_dependencies
)
```
The verifier rejects ambiguous protected-header or claim members, unknown
critical headers, `alg=none`, symmetric algorithms, algorithm/key mismatch,
incompatible JWK usage, ambiguous key selection, and signatures not valid
under exactly one accepted asymmetric key. It bounds the assertion, headers,
claims, subject, key identifiers, and authenticated key set before lookup or
logging. [FI-TRACE-ASSERTION-VALIDATION]
The exact `iss` selects an authenticated policy and key source; `iss` and at
least one `aud` value exactly match configured values. `sub` is a non-empty
bounded string. Each policy configures a non-negative finite `skew`, a positive
finite `maximum_assertion_age`, and, for `current-status`, a positive finite
`maximum_status_age`; a missing value denies. `exp` and `iat` are finite
NumericDate values satisfying `now < exp`, `iat <= now + skew`, and
`now < iat + maximum_assertion_age`. Optional `nbf` satisfies
`nbf <= now + skew`. Arithmetic is overflow-safe and equality at an expiry is
expired. [FI-TRACE-ASSERTION-VALIDATION]
The Nostr-key claim is named `nostr_pubkey`. When present it MUST be a
lowercase hexadecimal encoding of exactly one 32-byte Nostr public key; other
encodings and aliases deny. In `attested-key` enrollment policy and wherever
current matching issuer attestation is required, this exact claim MUST be
present and equal the proven actor; authorization claims or capabilities use a
closed bounded input set and deterministic canonical encoding. Unchecked claims
never enter the result. [FI-TRACE-VERIFIER-PARITY]
### Token class
Policy selects exactly one token class before parsing claims:
- **`at+jwt` access token**: a Buzz-resource access token whose protected
`typ` is exactly `at+jwt` and whose `aud` contains the configured Buzz
resource audience. This class selects tokens carrying the RFC 9068 `at+jwt`
type but validates them under this document's claim contract; it does not
implement the full RFC 9068 validation profile, and the long-form media type
`application/at+jwt` is not accepted;
- **dedicated Buzz assertion**: a separately minted assertion whose protected
`typ` is exactly `nip-fi+jwt`;
- **named compatibility access token**: absent or generic protected `typ=JWT`
only under an explicit issuer policy whose required and forbidden claims,
audience, issuer, key source, and validation rules are mutually exclusive
with every accepted ID-token and other JWT class.
OIDC ID Tokens always deny, even when `iss`, `aud`, and `sub` match. A generic
or absent type has no stock fallback. Failure under one class never triggers
validation under another. An `at+jwt` access token MUST contain one non-empty
bounded `client_id`. Issuer policy MUST distinguish a resource-owner token from a token
whose subject represents the OAuth client, including a client-credentials token,
using authenticated claim semantics and mutually exclusive validation rules. A
token that admits both interpretations denies. If client-subject tokens are
accepted, the issuer MUST guarantee that their `(iss, sub)` coordinates cannot
collide with resource-owner coordinates; otherwise that token class is
ineligible. Token class and every class-specific validation rule are inputs to
`assertion_policy_id`. [FI-TRACE-TOKEN-CLASS]
### Policy identity and snapshots
Core has exactly two semantic contract identities:
```text
assertion_policy_id = H(canonical assertion-policy contract)
transport_contract_id = H(canonical transport contract)
```
Each uses one implementation-defined but deterministic, versioned encoding and
collision-resistant hash within a deployment. `assertion_policy_id` covers the
canonical issuer, audience, token class, allowed algorithms, authenticated
key/status-source contracts, identity/key/claim mapping, time and size rules,
normalization, freshness class, and compiled verifier behavior. The verifier
fingerprint is an input, not a third identity. `transport_contract_id` covers
the client-attached field, parsing, attachment, context preservation, and
no-fallback semantics; a companion transport may define its own canonical
contract under that same identity slot. A semantic change changes exactly its
owning ID. [FI-TRACE-CONTRACT-IDENTITIES]
Mutable contents and deployment state are not contract identities. They remain
in `revalidation_dependencies`: authenticated assertion-snapshot version,
verification-key identity, key-snapshot hard deadline, optional status
source/version/deadline, binding/lifecycle/local-policy/resource versions,
proof and replay witnesses, and a confidential handle to the exact compact JWS.
Adding, removing, or replacing an accepted key changes the snapshot version,
not `assertion_policy_id`. Changed dependencies require revalidation under
current state; a retained key may continue, while an absent key denies.
Unknown-key refresh is bounded and coalesced and has no attacker-triggered
stale-key fallback. [FI-TRACE-JWKS-ADD] [FI-TRACE-JWKS-REMOVE]
The base contract compares the current authenticated snapshot and makes no
anti-rollback promise. A deployment claiming rollback prevention records a
separately authenticated monotonic floor and tests it. [deployment artifact:
assertion-policy review]
### Freshness class
Each policy declares exactly one server-owned freshness class, included in
`assertion_policy_id`:
- **`offline-jwt`** validates the JWT and authenticated key snapshot only.
`upstream_authority_deadline` is the minimum of `exp`,
`iat + maximum_assertion_age`, and the key-snapshot hard deadline. Token age
bounds assertions minted before revocation; it cannot bound an issuer that
continues minting accepted assertions afterward. Enabling this class therefore
requires deployment evidence that revocation stops new accepted issuance, and
discovery reports the unconditional residual bound as unknown (`null`). It
MUST NOT advertise a finite unconditional residual bound. [deployment
artifact: issuer revocation review]
- **`current-status`** additionally requires an authenticated witness
`(iss, sub, token_or_session_id?, active=true, observed_at, valid_until,
status_version, authenticated_source_id)`. Issuer, subject, and optional
session identifier exactly match the assertion. Ambiguous, unauthenticated,
inactive, or expired status denies. `valid_until` is finite and no later than
`observed_at + maximum_status_age`. The upstream deadline is the minimum of
the offline assertion deadlines and `valid_until`. Source outage cannot mint
or extend a witness; an already verified witness remains usable only until
its existing `valid_until`. [FI-TRACE-CURRENT-STATUS-STALE]
A current-status deployment advertises a tested positive
`maximum_residual_upstream_revocation_seconds`. Prepared evidence and leases
close within that value after upstream revocation, including a revocation racing
final admission. Poll/cache age, event-delivery and processing delay, and
enforcement delay all fit within the advertised value. A push implementation
may close authority sooner but cannot claim a value below its tested worst case.
[FI-TRACE-CURRENT-STATUS-REVOKED]
An external capability projection whose removal is required to close authority
within a declared revocation bound MUST enter authoritative local-policy state,
not `claims_or_capabilities` from the assertion. That state is reread during
preparation, final admission, and protected lease use. A deployment that carries
such a projection only in assertions cannot claim a revocation bound for its
changes. [FI-TRACE-CAPABILITY-REVOCATION]
Before enabling an issuer, the operator records authoritative evidence that
`sub` is stable for the account lifetime, never reassigned, and not intentionally
derived from mutable profile data. An issuer that cannot provide this property
is ineligible. [deployment artifact: issuer subject-stability review]
## Nostr proof and body semantics
The actor is always returned by fresh Nostr-proof validation, never by an
assertion or unsigned field. NIP-42 binds its AUTH event to the current
challenge, relay URL, connection, and freshness window. NIP-98 binds its event
to the exact server-resolved URL, method, and freshness window. All evidence
agrees with the same `D` and `R_t`. [FI-TRACE-DOMAIN-SPOOF]
Each protected HTTP operation declares in server policy whether its body is
authorization-relevant; clients cannot select the declaration.
For a relevant body, the NIP-98 event contains exactly one `payload` tag equal
to lowercase hexadecimal SHA-256 of the **body bytes**: the complete content
after transfer decoding and before any content decoding. Absence, duplication,
mismatch, validation of only a prefix, or substitution of the body bytes after
validation denies. For an irrelevant body, no
authorization decision, target, capability, or effect selector derives from a
body field not bound by NIP-98. A `payload` tag present on an operation whose
body is declared authorization-irrelevant is validated identically against the
body bytes; duplication or mismatch denies. [FI-TRACE-BODY-BINDING]
Every operation has finite body and spool bounds. A known oversized body is
rejected before hashing; a stream is rejected at octet `limit + 1`; admission
waits for EOF. Before EOF there is no application effect, replay mutation,
receipt, or partial digest authority. Quota failure cleans up staged bytes and
denies. [FI-TRACE-BODY-BOUNDS]
## Direct preparation
The following is normative pseudocode; every read is from authoritative state.
```text
PrepareDirect(request, assertion, proof):
(D, R_t, operation, resource) := ResolveTargetContext(request) or DENY
e := ValidateClientAttached(assertion, D, R_t) or DENY
k := ValidateNostrProof(proof, D, R_t) or DENY
R := SealActor(R_t, k)
i := e.identity
if e.asserted_key exists and e.asserted_key != k: DENY(key_mismatch)
atomically read B_D(i), B_D(k), T_D(i,k), Y_D(k), enrollment policy,
local policy, resource, and all dependency versions
if k in Y_D: DENY(key_revoked)
if (i,k) in T_D: DENY(pair_retired)
if B_D(i) = B_D(k) = binding(i,k):
proposal := existing(binding.version, binding.provenance)
else if B_D(i) exists or B_D(k) exists:
DENY(binding_conflict)
else if enrollment policy = attested-key:
if e.asserted_key != k: DENY(attestation_required)
proposal := enroll(i, k, attested-key)
else if enrollment policy = tofu:
proposal := enroll(i, k, e.asserted_key = k ? attested-key : tofu)
else:
DENY(binding_required)
EvaluateLocalPolicy(D, R, operation, resource, k,
e.claims_or_capabilities) or DENY
return PreparedAuthorization(evidence, proposal, witnesses, deadlines)
```
TOFU is optional private deployment posture and is not self-advertised. It
accepts that a stolen assertion for a never-enrolled identity can bind an
attacker's proven key; deployments enabling it retain a passing
FI-TRACE-TOFU-THEFT artifact. Binding provenance is immutable. A policy change
affects only future creation. [deployment artifact: TOFU risk review]
Preparation, including first-use enrollment, is read-only and produces no
authoritative mutation. [FI-TRACE-FINAL-DENIAL-NO-MUTATION]
## Final admission
A prepared value is consumed at most once. Final admission first requires an
exact domain, context, operation, resource, actor, and transport match; both
contract IDs unchanged; and every bound live. A changed dependency is reread
and re-evaluated from authoritative evidence. [FI-TRACE-PREPARED-STALE]
Two verified assertion results are **equivalent** when:
1. identity-class fields are byte-equal: `iss`, `sub`, asserted-key presence and
value, canonical claims/capabilities, `assertion_policy_id`, and
`transport_contract_id`;
2. each bounds-class deadline — every `authority_deadlines` member, the
key-snapshot hard deadline, and any status deadline — is live now and is no
later than its prepared value; and
3. provenance-class fields — snapshot version, verification-key identity,
status source and version, binding, lifecycle, local-policy, and resource
versions, proof and replay witnesses, the confidential JWS handle, cache
metadata, ordering, and retrieval time — are ignored after successful
current revalidation.
Every `revalidation_dependencies` member is bounds-class if it is a deadline
and provenance-class otherwise. Any new or unclassified assertion-content field
belongs to the identity class. A fresher assertion cannot silently extend a
prepared decision. [FI-TRACE-PREPARED-STALE]
Final admission atomically rereads binding, tombstone, revocation, enrollment,
policy, resource, status, replay, receipt, and invalidation witnesses;
recomputes the complete decision; claims applicable proof replay identities;
creates an eligible proposed binding; and appends its request-bound receipt and
required authorization evidence. All commit or none. A concurrent identical
enrollment may recompute as the same `existing` binding; conflicting enrollment
commits at most one winner. [FI-TRACE-CONCURRENT-ENROLLMENT]
A failed admission rolls back all authority mutation. The application operation
runs only after committed authorization. If it cannot share the transaction, a
request-bound idempotent receipt prevents the same proof from creating a second
effect. [FI-TRACE-FINAL-DENIAL-NO-MUTATION]
## Base lifecycle
Retirement, revocation, and rotation require separate privileged authority
bound to the exact domain, transition, identity, old binding version when
present, target key when present, and request. Each atomically rechecks current
state, appends immutable lifecycle history, and invalidates dependent leases
after commit. Every new target key supplies fresh target-bound Nostr proof and
any policy-required current matching issuer attestation. [FI-TRACE-LIFECYCLE-AUTHORITY]
- **RetirePair** removes one exact active binding and durably retires its pair.
- **RevokeKey** records the key as revoked even if inactive; if active, it also
removes the binding and retires that pair. Repeating the same authorized
revocation is idempotent.
- **Rotate** replaces one exact active binding with one unused, unrevoked,
non-retired target key, retires the old pair, and creates a fresh binding
version. The replacement provenance is `attested-key` when current matching
issuer attestation was validated and `provisioned` otherwise. Rotation does
not globally revoke the old key. Rotation continues one grant onto a new key
rather than establishing a new one: the replacement preserves every
profile-defined administrative bound carried by the binding it replaces, as an
opaque field core neither interprets nor clears. Only the authority that set
such a bound can change it. [FI-TRACE-LIFECYCLE-AUTHORITY]
Failure or stale state causes no partial mutation. Ordinary authorization cannot
perform or undo these transitions. Extended disablement, re-enablement,
provisioning, and administrative expiry are defined only by NIP-FI-LIFECYCLE.
## Request and session bounds
HTTP authority covers one exact request and is never reusable.
A WebSocket lease is scoped to one actor, domain, operation set, binding
version, normalized result, policy/resource versions, and invalidation
witnesses. Its deadline is the earliest assertion, upstream-authority,
key-snapshot, proof/connection, local-policy, and implementation deadline.
Arithmetic is overflow-safe and equality is expired. [FI-TRACE-LEASE-BOUND]
Before each protected use, the service checks actor, domain, operation,
resource, deadline, binding version, contract IDs, snapshot/status versions,
policy versions, and invalidation state. Changed dependencies require current
revalidation to an equivalent result; unreadable or ineligible state denies.
A lease for one key never authorizes another key on the same connection.
[FI-TRACE-MULTI-KEY-SESSION]
Expiry ends the lease, not the binding. Renewal requires a new connection with
a fresh assertion attached to its WebSocket upgrade, fresh Nostr proof,
preparation, and final admission; there is no in-band renewal path. Confidential
assertion revalidation material is destroyed on expiry, close, or invalidation.
## Rejection and privacy
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 authoritative dependency is
unreadable, never any per-principal fact. Replay status is a function of
committed per-principal server state, not of the supplied evidence alone;
replayed evidence is therefore classed `authorization_denied`, indistinguishable
from any other private-state denial, so that resubmitting captured evidence
reveals nothing about whether the original request committed. Under the
private-posture rule, even `key_mismatch` joins the private-state anonymity set.
| Private condition | Public class | Nostr prefix and exact text | HTTP response |
|---|---|---|---|
| assertion/proof absent | `missing_evidence` | `auth-required: authentication required` | `401`; `WWW-Authenticate: Nostr`; `Content-Type: text/plain; charset=utf-8`; `authentication required\n` |
| malformed, invalid, or expired evidence | `evidence_rejected` | `restricted: evidence rejected` | `403`; `Content-Type: text/plain; charset=utf-8`; `evidence rejected\n` |
| replayed evidence; key mismatch; attestation required; binding conflict; retired pair; revoked key; lifecycle gate; binding required/expired; local policy denial | `authorization_denied` | `restricted: authorization denied` | `403`; `Content-Type: text/plain; charset=utf-8`; `authorization denied\n` |
| required current dependency unreadable | `authorization_unavailable` | `restricted: authorization unavailable` | `503`; `Content-Type: text/plain; charset=utf-8`; `authorization unavailable\n` |
Nostr text is the exact UTF-8 text after an applicable NIP-42/NIP-01 prefix.
A denial decided on a WebSocket upgrade request, before any NIP-42 proof
exists, is the HTTP response in the table, sent instead of `101`; a denial
decided after the connection is established is the Nostr text. For HTTP, the
compared denial contract is closed over the
status, complete body,
and exact values of only the header fields named in the table; header order and
other fields are outside that contract and their values cannot depend on the
private condition. The body is the shown UTF-8 bytes with one LF and no other
bytes. The `Nostr` challenge satisfies RFC 9110 Section 15.5.2.
Responses contain no free text, reason code, request ID, issuer, subject, key,
claim, binding state, enrollment posture, token material, or timing hint. All
private conditions in `authorization_denied` produce byte-identical responses.
[FI-TRACE-DENIAL-ORACLE]
NIP-FI defines no public identity projection. Public events, tags, filters,
discovery, responses, logs, metrics, and traces contain no raw assertions or
unredacted `iss`, `sub`, email, display name, or private claim. Access-controlled
authoritative stores retain only what enforcement and investigation require.
A separate presentation protocol cannot confer NIP-FI authority.
[FI-TRACE-PRIVACY-NONPUBLIC]
## Discovery
A relay SHOULD advertise core support in NIP-11 as:
```json
{
"limitation": { "federated_identity": true },
"federated_identity": {
"core": "client-attached",
"assertion_freshness": {
"class": "offline-jwt",
"maximum_residual_upstream_revocation_seconds": null
}
}
}
```
NIP-FI-EDGE owns the optional `edge_transports` member and its exact type,
placement, and value semantics. For `current-status`, the final value is a tested
positive integer. Discovery never states enrollment mode or TOFU posture and
never exposes issuer URLs, audiences, claim names, tenant IDs, or
deployment-local identifiers. For a fixed
set of claimed profiles, the complete public discovery output is byte-identical
for every enrollment policy, including `attested-key`, private `tofu`, and any
companion profile mode: no field, flag, value, omission, ordering, or object shape
may distinguish the configured mode. Profile documents own only non-enrollment
public claims.
[FI-TRACE-DISCOVERY-PRIVATE]
## Worked example (non-normative)
A protected HTTP POST under `client-attached` with NIP-98 proof and an
authorization-relevant body. Credentials are elided; the NIP-FI-CONF exit
fixture pins the complete request compared objects.
```text
POST /media HTTP/1.1
Host: relay.example
Nostr-Federated-Identity: Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCIs...
Authorization: Nostr eyJpZCI6IjE1ZTI3ZDc0Li4uIiwicHVia2V5IjoiOTljNzQ4Li4u...
Content-Type: application/octet-stream
Content-Length: 4
abcd
```
The bearer JWS validates under the configured assertion policy: exact `iss`
and `aud`, token class `at+jwt`, live time claims, and `nostr_pubkey` equal to
the NIP-98 event's `pubkey`. The NIP-98 event binds the server-resolved method
and URL, and its single `payload` tag equals the SHA-256 of the four body
bytes. Admission then follows Direct preparation and Final admission; success
returns the application response, and every failure class returns exactly the
bytes fixed in the Rejection table. On a WebSocket upgrade the same header
attaches to the upgrade request and NIP-42 supplies the proof after connect.
## Core behavioral oracles
A core claim covers every applicable oracle below at one implementation and
policy revision. NIP-FI-CONF defines evidence and mutation-adequacy rules.
| ID | Required outcome |
|---|---|
| `FI-TRACE-TRANSPORT-CLOSED` | Exact one-header input succeeds; missing, repeated, combined, malformed, mixed, URL, and fallback variants deny. |
| `FI-TRACE-ASSERTION-VALIDATION` | Valid boundary input passes; each signature, key-selection, issuer, audience, time, size, ambiguity, and missing-configuration negative denies. |
| `FI-TRACE-TOKEN-CLASS` | An `at+jwt` access token and a dedicated `nip-fi+jwt` assertion pass only their selected class. ID tokens, wrong/generic types outside a named compatibility policy, client-only audiences, absent or ambiguous `client_id`, resource-owner/client-subject ambiguity, and every attempted cross-class fallback deny. |
| `FI-TRACE-CONTRACT-IDENTITIES` | Mutate each assertion semantic, transport semantic, and mutable dependency independently: semantic mutations change only their owning contract ID; snapshot/binding/lifecycle/policy/resource/status mutations change neither ID but force current revalidation. |
| `FI-TRACE-VERIFIER-PARITY` | Equal authoritative input and policy produce the same canonical normalized result. |
| `FI-TRACE-JWKS-ADD` | Retained-key rotation revalidates successfully under the changed snapshot version. |
| `FI-TRACE-JWKS-REMOVE` | Evidence and leases under a removed key deny after snapshot change. |
| `FI-TRACE-CURRENT-STATUS-REVOKED` | Revocation, including one racing final admission, closes authority within the advertised tested bound. |
| `FI-TRACE-CURRENT-STATUS-STALE` | Inactive/ambiguous status denies; an issuer, subject, or session-identifier mismatch denies; expiry equality, outage, delayed events, and changed status versions cannot mint or extend a witness. |
| `FI-TRACE-CAPABILITY-REVOCATION` | Removal of a revocation-bounded external capability projection from authoritative local policy closes prepared evidence and lease use within the declared bound; assertion-only projection cannot satisfy this oracle. |
| `FI-TRACE-BODY-BINDING` | Exact complete relevant body passes; absent/duplicate/mutated/partial/substituted payload variants deny without effects; a payload tag on an irrelevant-body operation validates identically and denies on duplication or mismatch. |
| `FI-TRACE-BODY-BOUNDS` | Oversized, over-quota, and pre-EOF variants deny with bounded work, cleanup, and no effects. |
| `FI-TRACE-DOMAIN-SPOOF` | Client routing and forwarded authority cannot replace server-owned context. |
| `FI-TRACE-ASSERTION-KEY-MISMATCH` | Mismatch denies with no mutation and the private-state response. |
| `FI-TRACE-BINDING-CONFLICT` | A binding conflict denies without replacing either existing binding. |
| `FI-TRACE-TOMBSTONE-REPLAY` | Fresh eligible evidence for a retired pair or revoked key denies without recreation. |
| `FI-TRACE-ASSERTION-REFRESH` | Fresh evidence reuses the same eligible durable binding after prior assertion expiry. |
| `FI-TRACE-PREPARED-STALE` | Changed identity-class witnesses or extended bounds deny; provenance-only rotation revalidates. |
| `FI-TRACE-CONCURRENT-ENROLLMENT` | Identical first use converges; conflicting first use commits at most one winner. |
| `FI-TRACE-FINAL-DENIAL-NO-MUTATION` | Every failed phase leaves all authoritative stores and effects unchanged. |
| `FI-TRACE-LIFECYCLE-AUTHORITY` | Unprivileged/stale transitions deny; authorized retirement/revocation/rotation is atomic. |
| `FI-TRACE-LEASE-BOUND` | A lease ends at its earliest bound; equality at any bound is expired. |
| `FI-TRACE-MULTI-KEY-SESSION` | One actor's lease never authorizes another key on the same connection. |
| `FI-TRACE-DENIAL-ORACLE` | Each private row produces its exact fixed bytes on every surface where its condition can be decided — HTTP, a WebSocket upgrade, or after connect; all private-state rows compare byte-identical. |
| `FI-TRACE-DEPENDENCY-FAIL-CLOSED` | Each unreadable authoritative dependency denies. |
| `FI-TRACE-AUTHORITY-UNIFORM` | Every protected ingress reaches one current final-admission authority. |
| `FI-TRACE-CROSS-DOMAIN-COLLISION` | Equal subjects across issuers and equal pairs across domains remain distinct. |
| `FI-TRACE-PRIVACY-NONPUBLIC` | Private identity does not enter public surfaces. |
| `FI-TRACE-DISCOVERY-PRIVATE` | Complete discovery bytes remain identical across attested-key, TOFU, and companion enrollment modes. |
| `FI-TRACE-TOFU-THEFT` | Stolen-assertion first use denies unless private TOFU is enabled and the attacker also proves its chosen key. |
## Relationship to other work (non-normative)
NIP-FI binds an access token to a key the resource server itself verifies, the
goal DPoP (RFC 9449) and mTLS-bound tokens (RFC 8705) reach through a `cnf`
claim. Here the proof is the NIP-42 or NIP-98 event the relay already
validates, so no second proof is defined and the issuer need not attest the
key; `nostr_pubkey` is the optional `cnf` analogue. Unlike those profiles the
binding is durable server state rather than a per-token claim: a stolen
assertion cannot reach an enrolled identity without its key, and revocation is
a local fact rather than a token-lifetime race. One identity, one key per
domain is stricter than WebAuthn's many-credentials-per-account model because
the Nostr key is itself the public identity; additional devices do not create
additional active bindings. Two contract identities plus explicit dependency
versions exist because folding a mutable key snapshot into policy identity would make benign
rotation change policy lineage, while omitting it would let evidence under a
removed key survive. Denial responses deliberately collapse the conditions that
RFC 6750 error codes distinguish. `trusted-proxy-hmac-v2` in NIP-FI-EDGE is a
fixed-component request MAC in the family of HTTP Message Signatures (RFC 9421)
and AWS SigV4, without negotiation and with length-prefixed canonicalization.
## Security considerations
Issuer compromise can impersonate a principal but cannot prove an uncompromised
bound Nostr key. Assertion theft cannot use an existing binding without that
key; private TOFU intentionally retains first-use theft risk. Snapshot
revalidation limits removed-key reuse but the base policy accepts authenticated
key-source rollback as residual issuer risk. Two-phase admission closes the
binding and policy TOCTOU window only when every authoritative witness is reread
atomically. Availability failures deny rather than degrade to Nostr-only access.
## Sources
- NIP-42 authentication: <https://github.com/nostr-protocol/nips/blob/6d2979b3f503a8539c983efbcdcf901bbcf9ed23/42.md>
- NIP-98 HTTP authentication: <https://github.com/nostr-protocol/nips/blob/6d2979b3f503a8539c983efbcdcf901bbcf9ed23/98.md>
- JWT BCP: <https://www.rfc-editor.org/rfc/rfc8725>
- JWT access-token profile: <https://www.rfc-editor.org/rfc/rfc9068>
- DPoP: <https://www.rfc-editor.org/rfc/rfc9449>
- OAuth 2.0 mTLS client certificate-bound tokens: <https://www.rfc-editor.org/rfc/rfc8705>
- HTTP Message Signatures: <https://www.rfc-editor.org/rfc/rfc9421>
- Non-normative composed model: [NIP-FI-MODEL.md](NIP-FI-MODEL.md)