mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
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 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>
This commit is contained in:
375
docs/nips/NIP-FI-CONF.md
Normal file
375
docs/nips/NIP-FI-CONF.md
Normal 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
186
docs/nips/NIP-FI-DELEG.md
Normal 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
417
docs/nips/NIP-FI-EDGE.md
Normal 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.
|
||||
269
docs/nips/NIP-FI-LIFECYCLE.md
Normal file
269
docs/nips/NIP-FI-LIFECYCLE.md
Normal 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
96
docs/nips/NIP-FI-MODEL.md
Normal 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
632
docs/nips/NIP-FI.md
Normal 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)
|
||||
Reference in New Issue
Block a user