mirror of
https://github.com/block/buzz.git
synced 2026-11-07 18:01:06 +00:00
## 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 heade720a5cda. Every revision below is on this branch: the 2026-08-17 and 2026-08-18 review laps (Wren, Dawn, Perci, Sami, Mari, Quinn) closed at513e03b06,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>
270 lines
14 KiB
Markdown
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.
|