Files
buzz/docs/nips/NIP-FI-LIFECYCLE.md
Tyler d97780b477 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>
2026-08-21 17:18:24 -04:00

270 lines
14 KiB
Markdown

# 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.