mirror of
https://github.com/block/buzz.git
synced 2026-10-27 20:41:57 +00:00
## What this is An alternative shape for the NIP-FI document family, built on top of #5946 at its current head `772ba7a72` (so it carries R1–R12 and diffs cleanly against it). Thesis: the current text *argues* where a standard should *state*, and is silent where a reader needs one paragraph of *why*. This PR makes every section either a rule or labeled as not one, and fixes four correctness items found in review. Base: `772ba7a72` (= `eva/nip-fi-comprehensive`). Eight FF commits, no rebase. Three files. Oracle census unchanged at **58** (30/6/4/11/7). **Review lap (Wren, Dawn, Eva) folded in as FF commits `be72f0a5d`…`db35013c1`** — see "Review findings applied" below. ## Changes ### NIP-FI-CONF — 4,325 → 3,088 words (−29%), no obligation lost - Denial fixtures 1,516 → ~740 words. Same 16-row table. Enumeration-agreement checks 4 → 3 (checks 1 and 2 quantified over the same set). Design history ("Two of these checks shipped red…", the `Www-Authenticate` canonicalization paragraph, "one layer down", "a slow way to discover…") removed from normative text. - Interoperability exit test 934 → ~620 words. Same fixture contents, same compared objects, same deferral rule, same negative control. - Release gate restructured as a list; identical conjuncts. - **BCP 14 keyword census 37 → 36.** Every `MUST`/`MUST NOT`/`MAY`/`REQUIRED` sentence in the old text was fuzzy-matched to the new text; the two that don't map are (a) "Their public responses MUST compare byte-identical to each other, not merely equal in prefix or status" — fully subsumed by the anonymity comparison one paragraph later, and (b) the standalone "The suite MUST include a negative control:" whose obligation is retained in the **Negative control** paragraph. Reviewers: please try to find a third. ### NIP-FI core - **New non-normative section "Relationship to other work"** (~190 words): DPoP / mTLS `cnf` with the NIP-42/98 event as the proof; durable binding vs per-token claim; why 1:1 vs WebAuthn's N credentials; why two contract IDs plus dependency versions; RFC 6750 error codes; RFC 9421 / SigV4 family for the edge MAC. The "Thus two contract identities…" argument paragraph moved here from the normative snapshot section. Three source links added. - **Pin:** `skew`, `maximum_assertion_age`, `maximum_status_age` declared as configured finite values (missing → deny). Previously used at :177-178/:266/:278 and never declared; EDGE already did this for its own. - **Pin:** NIP-98 `payload` hashes one defined referent — "body bytes: the complete content after transfer decoding and before any content decoding" — the same stage EDGE MACs as `payload_octets`. Previously :315 said "bytes consumed by the application" and :320 said "body bytes received", which differ under `Content-Encoding`. **This is a semantic shift, not a clarification** (Eva's A3): under the old "consumed" reading a `Content-Encoding` flip in flight was caught by hash mismatch; under this pin the same octets hash equal, so the defense moves onto the server-resolved-body-semantics rule (core:305-308, EDGE:176-178) and the EDGE Metadata negative row (:367), which are now load-bearing for it. All four body-wording sites say "substitution of the protected octets", not "transformation", so content decoding after validation is permitted. - **Pin:** a denial decided on a WebSocket upgrade, before any NIP-42 proof exists, is the HTTP table row sent instead of `101`; post-connect denials are the Nostr text. `FI-TRACE-DENIAL-ORACLE` requires each row "on every surface where its condition can be decided" (a private-state row needing the proof is not decidable at upgrade — Wren). CONF exit list aligned. ### NIP-FI-EDGE - **R12 exception class-scoped** (Dawn's finding): "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" instead of naming the field. Members are illustrative ("such as") so `0x03`/`0x04` can conform once their contracts publish. Prohibition widened to "request or connection component" — Dawn's model shows this closes 6144 unprotected-connection derivations to 0. Requirement 3 gets the matching carve-out. - **`Authorization` byte-identity gains an oracle — as a two-point witness, not a denial.** EDGE:62-65 makes altering it nonconformant; no oracle named it. First attempt said "altered … denies"; Eva (A1) and Dawn's `a1_oracle.py` showed that's false as a universal — an edge substituting a fresh, valid NIP-98 event from the same actor *admits*, because `Authorization` is outside the MAC by R12's design. `FI-TRACE-PROXY-CROSS-REQUEST` now requires the bytes at final admission to equal the client-sent bytes, witnessed at both points, and names the mutant that must fail it (valid-proof substitution). The Request matrix row no longer claims a denial for it. ## Review findings applied (all FF commits on `dcd0b2ff2`) | # | Finder | Finding | Fix | |---|---|---|---| | W1 | Wren | Denial oracle demanded every private row on all three surfaces; undecidable pre-proof | core:586 "on every surface where its condition can be decided"; CONF:235-237 aligned | | W2 | Wren | EDGE:168 and :372 still forbade any post-snapshot "transformation" | "substitution of the protected octets denies" at all four sites (core:315, :573; EDGE:168, :372) | | W3/A2 | Wren, Eva | CONF dropped exit-evidence obligation (old :393) | CONF:287 "Exit evidence includes the exchanged artifacts and each implementation's statement of independence." | | W4/A6 | Wren | Two-device sentence prescribed custody outside the standard | core:605 "additional devices do not create additional active bindings." | | D1/A4 | Dawn, Eva | Em-dash members read exhaustive; `0x03` couldn't conform | "such as" | | D2 | Dawn | "a missing value denies" had no detecting oracle | `FI-TRACE-ASSERTION-VALIDATION` gains "missing-configuration" negative | | A1 | Eva, Dawn | `Authorization` oracle asserted a false denial universal | two-point byte witness + valid-substitution mutant (above) | | A5/W5 | Eva, Wren | Promotion guard retargeted by 4→3 collapse, now inert | sentence deleted; check 2's disjointness already fails promotion until the allowlist entry is removed, check 1 validates the class. CONF MUST NOT 9→8, deliberate | | D3 | Dawn | Replacement sentence said "only check 2's disjointness fails" — false in 2/4 promotion cases (check 1 fails too when classes disagree); "then" implied an ordering the independent conjuncts don't have | both words dropped (`624429bd8`) | Not taken this lap (predate this PR): EDGE:180 `maximum_provenance_age` missing-config oracle; `0x04` Blossom row says "authorization event" not "Nostr", so the class exception can't reach it until its contract is written. ## Not changed LIFECYCLE, DELEG, MODEL: zero bytes. Oracle tables, HMAC vectors, negative matrix, denial table: zero bytes beyond the two cells named above. ## Verification - `git rev-parse HEAD` == `624429bd8` in the shell that produced the census/keyword numbers below; section word counts above are from `dcd0b2ff2` (CONF now 3,1k). - Oracle census: `rg -c '^\| \`FI-[A-Z0-9-]+\` \|'` per file → 30/6/4/11/7. - Distribution markers: 0 hits across all six docs and the commit message. - Keyword census and sentence mapping: script in the PR thread on request. ## Open questions 1. ~~Two-device sentence~~ — closed by Wren's wording. 2. ~~R12 widening~~ — Dawn's model: net stricter than `772ba7a72`. 3. ~~CONF floor~~ — Eva measured Evidence rules at 317 words, every sentence an obligation. Floor reached. 4. ~~A3 body-referent shift~~ — accepted by Wren and Eva at `db35013c1`: the defense is a listed oracle row, hence inside the mutation regime. ## Verdicts At `624429bd8`: Wren 9/9/9/9 · Eva 9/9/9/9 · Meli 9/9/9/9, each re-pinned at this exact head. Dawn's three instruments (conf/a1/r12) rc=0 at this head. CI 14 pass / 0 fail / 9 skipped. Zero known falsehoods in the text. Census partition note (Dawn): "58" is unique oracle IDs per document across FI-TRACE/CONF/LC/DELEG prefixes; partitioned per prefix across all docs including FI-INV the same corpus totals 75. Quote the partition with the number. --------- Signed-off-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz> Co-authored-by: Meli <5aaa86bce934fc3445fc254aab560a40923f10252f92107e665073dede0e04d3@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -20,13 +20,10 @@ 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, and a
|
||||
public claim of it would be an unverifiable assertion about the server's own
|
||||
testing.
|
||||
|
||||
This profile defines no wire behavior, denial mapping, invariant, or admission
|
||||
rule. Where it names one, NIP-FI core or the owning profile is normative.
|
||||
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
|
||||
|
||||
@@ -46,30 +43,26 @@ A conformance claim names exactly one immutable tuple:
|
||||
```
|
||||
|
||||
Changing any element creates a new claim. Results from one tuple MUST NOT be
|
||||
carried into another. The governing document revision and exit fixture digest
|
||||
are in the tuple because the same evidence means different things under
|
||||
different text: a claim that does not name the revision it was judged against
|
||||
is unfalsifiable once the specification moves, and a suite that does not name
|
||||
its fixture cannot be shown to have run the pinned inputs. 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 defined in
|
||||
**Interoperability exit test**. Blank, skipped, expected-failure, and
|
||||
not-run results cannot support a claim (`FI-CONF-CLAIM-COMPLETE`).
|
||||
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.
|
||||
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
|
||||
outcome and the observed outcome, 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
|
||||
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
|
||||
@@ -84,31 +77,27 @@ 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 output distinguishes
|
||||
enrollment mode, so a discovery witness for that oracle cannot exist; requiring
|
||||
one would make the oracle unsatisfiable. Discovery invariance is proved
|
||||
separately by `FI-TRACE-DISCOVERY-PRIVATE`, which compares complete discovery
|
||||
bytes across enrollment modes.
|
||||
only; under the private-posture rule no discovery witness for enrollment mode
|
||||
can exist. Discovery invariance is proved separately by
|
||||
`FI-TRACE-DISCOVERY-PRIVATE`.
|
||||
|
||||
Deployment-obligation requirements — those marked in core or a profile as
|
||||
`[deployment artifact: ...]` — 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.
|
||||
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, and the
|
||||
interoperability exit test cannot run on redacted inputs.
|
||||
`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 passes trivially: it compares a class
|
||||
against itself. The enumeration below is the required fixture set
|
||||
(`FI-CONF-DENIAL-FIXTURES`). Its public-class column restates NIP-FI core,
|
||||
which owns that mapping and the exact response bytes.
|
||||
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 |
|
||||
|---|---|---|---|
|
||||
@@ -129,289 +118,188 @@ which owns that mapping and the exact response bytes.
|
||||
| 15 | `delegation_not_current` — owner or relationship no longer current | `authorization_denied` | NIP-FI-DELEG |
|
||||
| 16 | `dependency_unreadable` | `authorization_unavailable` | core |
|
||||
|
||||
The names in the private-condition column are fixture identifiers for this
|
||||
enumeration. Some are the symbols core's preparation pseudocode denies by name;
|
||||
that set is read from core, never restated here, because a copied list is state
|
||||
that drifts and the whole subject of this section is that unguarded restatements
|
||||
rot. `policy_denied` and `dependency_unreadable` are core's conditions expressed
|
||||
only in prose — a bare policy denial and `FI-INV-14` fail-closed — and core is
|
||||
not required to name them symbolically; they are the only two core rows so
|
||||
exempted. The remaining rows name conditions in the owning profiles' literal
|
||||
private-denial-condition tables. None is a wire value, and a deployment MAY use
|
||||
different private reason codes
|
||||
internally as long as every enumerated condition has a fixture.
|
||||
|
||||
Every row whose public class is `authorization_denied` is in the private-state
|
||||
anonymity set. Their public responses MUST compare byte-identical to each other,
|
||||
not merely equal in prefix or status.
|
||||
Rows for an unclaimed profile are `not-applicable` with absence evidence. A
|
||||
profile that introduces a new private condition MUST add its row; an
|
||||
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.
|
||||
|
||||
The anonymity comparison is **wider than the interoperability compared object
|
||||
defined below, and deliberately so**. Between two private conditions on the same
|
||||
implementation, every response byte as transmitted MUST agree — including
|
||||
transfer framing, and not only the content — except values a server cannot
|
||||
hold constant across two instants, such as `Date`. It is not limited to the
|
||||
header fields core names. The narrower object below exists because two
|
||||
*different* implementations cannot be required to agree on fields core does not
|
||||
pin; that reasoning does not apply within one implementation, where any field
|
||||
varying by private condition is a disclosure whatever its name. A suite that
|
||||
reuses the interoperability object here would pass an implementation that
|
||||
returns its private reason code in an unnamed header, or one that varies its
|
||||
chunk boundaries by private condition.
|
||||
**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:
|
||||
|
||||
**Enumeration agreement.** The preceding paragraph makes a quantified claim
|
||||
about this table, and a fix verified against the one row it changes can still
|
||||
falsify it. `policy_denied` and `dependency_unreadable` are the **prose-only
|
||||
allowlist**: core's conditions that core is not required to name symbolically.
|
||||
Both sets here are named by symbol, never by row number, because this table is
|
||||
required to grow and every positional reference silently retargets when it does.
|
||||
The suite MUST check, mechanically at the claimed head
|
||||
(`FI-CONF-DENIAL-FIXTURES`):
|
||||
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.
|
||||
|
||||
1. every symbol core denies by name has a row here, attributed to core;
|
||||
2. every symbol core denies by name carries the same public class — quantified
|
||||
over core's symbolic set, not over all core-attributed rows, since
|
||||
`dependency_unreadable` is core-attributed and correctly
|
||||
`authorization_unavailable`; and
|
||||
3. the set of symbols named by core-attributed rows equals core's symbolic
|
||||
denial set together with the allowlist, exactly, and the allowlist is
|
||||
disjoint from core's symbolic denial set; and
|
||||
4. for each claimed profile that owns a literal private-denial-condition table,
|
||||
the set of `(identifier, public class)` pairs in that table equals the set of
|
||||
pairs in rows attributed to that profile here, exactly. A row with multiple
|
||||
profile owners contributes its pair to each owner named in that cell.
|
||||
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.
|
||||
|
||||
Every check above MUST be run against the unmutated document and be green before
|
||||
any mutant is scored. A check that is red on a conforming document detects
|
||||
nothing: it cannot be observed to flip, so every mutant reads as caught. Two of
|
||||
these checks shipped red for exactly that reason.
|
||||
**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.
|
||||
|
||||
Checks 3 and 4 keep copied enumerations honest in both directions. Check 4
|
||||
catches a profile condition renamed, added, removed, reclassified, or attributed
|
||||
to the wrong owner without the matching fixture-table change. It is run for each
|
||||
claimed owning profile. Rows for an unclaimed profile remain `not-applicable`.
|
||||
**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.
|
||||
|
||||
Check 3 keeps the core allowlist honest in both directions. Its equality half
|
||||
catches
|
||||
a core-attributed row core never emits. Its disjointness half is what makes
|
||||
promotion visible: if a later core turns `policy_denied` or
|
||||
`dependency_unreadable` into a named symbol, every other check still passes —
|
||||
the row is already attributed to core — and only disjointness fails, leaving the
|
||||
stale allowlist entry as the thing to delete. Promotion MUST NOT be applied to
|
||||
`dependency_unreadable` without moving it out of the set check 2 quantifies
|
||||
over; it is the one core condition whose public class differs.
|
||||
|
||||
**Compared object.** This object governs the interoperability comparison between
|
||||
two implementations. The anonymity comparison above is wider. Byte-identity is
|
||||
asserted over the response bytes an implementation chooses, which excludes bytes
|
||||
a conforming HTTP server cannot hold constant. Over Nostr the compared object is
|
||||
the complete relay message excluding only the event or subscription identifier
|
||||
echoed from the request, encoded as compact JSON with no insignificant
|
||||
whitespace, per NIP-01's serialization rules. Over HTTP the compared object is
|
||||
exactly what NIP-FI core pins: the status code, the complete body, and the exact
|
||||
values of only the header fields core's denial table names. Header field *names*
|
||||
are matched case-insensitively per RFC 9110 Section 5.1; their values are
|
||||
compared exactly. The compared body is the *content* per RFC 9110 Section 6.4 —
|
||||
after transfer-decoding, chunk framing and trailer fields excluded — not the
|
||||
message body on the wire. That reading is scoped to this interoperability object
|
||||
and does not reach the anonymity comparison above, which stays over transmitted
|
||||
octets. `Content-Length` is deliberately not pinned: framing is the sender's
|
||||
choice and pinning it would widen the object for no privacy gain.
|
||||
Header order and unnamed header fields are outside it, and their values MUST NOT
|
||||
depend on the private condition — which the anonymity requirement above already
|
||||
demands and tests directly.
|
||||
|
||||
Each reading is stated because an independently written conforming
|
||||
implementation diverges on it by language default, not by error: a canonicalizing
|
||||
HTTP library emits `Www-Authenticate`, a server that sets no `Content-Length`
|
||||
frames the body as chunked, a JSON encoder inserts spaces after `,` and `:`, and
|
||||
two servers emit different automatic fields — `Server`, `Connection` — in
|
||||
different orders. Comparing those would fail every conforming pair, and an
|
||||
oracle no conforming implementation can satisfy is a defect in this document
|
||||
rather than evidence about either implementation. The compared object is
|
||||
therefore closed over what core names and nothing more; if core later pins an
|
||||
additional field, it joins with no edit here. `Date` needs no special exclusion,
|
||||
since core does not name it. Any field an implementation must exclude despite
|
||||
core naming it MUST be reported with the reason it cannot be held constant, and
|
||||
its value MUST be independent of the private condition.
|
||||
|
||||
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.
|
||||
**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.
|
||||
|
||||
The suite MUST include a negative control: an implementation deliberately
|
||||
patched to vary its denial response by private condition MUST fail this oracle.
|
||||
Without it the suite asserts that it works instead of demonstrating it.
|
||||
**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
|
||||
|
||||
Naming an oracle for a requirement proves the requirement is claimed, not that
|
||||
the oracle can fail. A requirement whose oracle cannot fail is untested and
|
||||
reads as tested, which is worse than an acknowledged gap.
|
||||
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.
|
||||
|
||||
The denominator is the **listed oracle**. An oracle table is identified by its
|
||||
rows: each names exactly one complete literal oracle identifier in its first
|
||||
cell, with no shorthand and no name left to inference. The denominator is every
|
||||
such row in NIP-FI core, in each claimed normative profile, and in this document
|
||||
when CONF is claimed — selected by that first cell, not by section title, since
|
||||
the tables do not share one. Enumerating it is reading rows, so two readers
|
||||
obtain the same set. It is not the set of normative sentences, RFC 2119
|
||||
keywords, or invariant labels: none is enumerable without judgement, and a
|
||||
denominator two readers count differently decides how much of the specification
|
||||
is tested at all.
|
||||
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 each listed oracle in core and each claimed profile, 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.
|
||||
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.
|
||||
|
||||
While `FI-CONF-INTEROP-EXIT` is validly deferred under **Interoperability exit
|
||||
test**, it remains in claim completeness but is excluded from the mutation and
|
||||
global-control obligations of this section: its failing output cannot exist
|
||||
without the run itself. 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. It is not a second
|
||||
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.
|
||||
|
||||
Five rules make the mutant meaningful:
|
||||
|
||||
1. **One at a time.** Mutants are applied singly against an otherwise unmodified
|
||||
implementation. Layered defenses mask each other: a guard looks covered
|
||||
because a different guard denies first.
|
||||
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 some other oracle establishes coverage for neither.
|
||||
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. Otherwise one broad mutant
|
||||
discharges several obligations at once and the count reads complete while
|
||||
the coverage is not.
|
||||
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. A mutant behind a bound,
|
||||
length field, or earlier denial that no fixture ever passes is never
|
||||
exercised, and the suite reports clean on an implementation that is
|
||||
provably broken.
|
||||
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 from both sides. A deny-everything
|
||||
implementation MUST fail every positive oracle, proving each oracle has a
|
||||
positive arm. An allow-everything implementation MUST fail every negative
|
||||
oracle, proving each has a negative arm. Neither control substitutes for
|
||||
per-entry mutants; an implementation can pass both while violating any
|
||||
individual requirement.
|
||||
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 document alone is
|
||||
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
|
||||
both transports, compared over the object defined under **Denial fixtures**.
|
||||
- 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 and reference implementations, not about
|
||||
inputs. Two implementations given different issuers, keys, or clocks cannot
|
||||
produce equal bytes however correct both are, so the run is parameterized by a
|
||||
**shared exit fixture** that both sides load and neither side authors:
|
||||
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 expressed
|
||||
as complete pre-signature protected-header and claim-set JSON values —
|
||||
including `alg`, `typ`, `kid`, and every member the policy allows, and fixed
|
||||
- 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`, so both sides derive the same actor and the same event id;
|
||||
- one frozen evaluation instant, and the skew and lifetime bounds in force; and
|
||||
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 shared exit fixture is authored by this document's editors, not
|
||||
by any claiming implementation, and MUST be published as a single file in the
|
||||
same repository as these documents, at `docs/nips/fixtures/nip-fi-conf-exit.json`,
|
||||
together 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 file is not yet published, the exit fixture digest
|
||||
element of the claim tuple records the reserved value
|
||||
`pending-canonical-fixture`; that value is valid only in a claim whose
|
||||
`FI-CONF-INTEROP-EXIT` result is `deferred`. Publication of the fixture changes
|
||||
the element and therefore creates a new claim.
|
||||
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.** A minted request cannot be compared as bytes. Two
|
||||
conforming implementations may produce different signature octets for the same
|
||||
semantic input — randomized `ES256` and BIP-340 with fresh auxiliary input do,
|
||||
while deterministic ECDSA and fixed-aux BIP-340 need not — and no document here
|
||||
pins JWS or JSON member order. An object requiring equal octets would therefore
|
||||
fail conforming pairs, which is the defect this document names one layer down.
|
||||
The compared object is the **signing inputs**:
|
||||
**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.
|
||||
|
||||
- for a NIP-98 proof over HTTP and a NIP-42 proof over the WebSocket upgrade,
|
||||
the NIP-01 serialization the event id is taken over, which NIP-01 fixes
|
||||
byte-for-byte; each transport's proof is compared against its own transport's
|
||||
serialization, not across transports; and
|
||||
- for the assertion, the decoded protected header and claim set compared as
|
||||
JSON values, with member order excluded.
|
||||
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.
|
||||
|
||||
Signature octets are excluded because conforming implementations need not agree
|
||||
on them, not because they disagree about this document; the mutual-acceptance
|
||||
requirement below tests them instead. A divergence in a signing input is a
|
||||
divergence in what this document told each side to sign, which is what the exit
|
||||
test exists to detect.
|
||||
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.
|
||||
|
||||
Every value the compared object depends on MUST be pinned here. A value left to
|
||||
the implementation is a divergence the test will attribute to a defect in this
|
||||
document, which is the correct disposition but a slow way to discover a missing
|
||||
fixture field.
|
||||
|
||||
The exchanged artifact per case is the complete request frame and the complete
|
||||
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
|
||||
message and the complete relay message. Both sides emit whole frames even
|
||||
though the compared objects are narrower, because the unnamed fields are what
|
||||
the reader needs in order to explain a mismatch.
|
||||
|
||||
The evidence is the produced bytes and a statement of independence; the claim
|
||||
tuple already pins the fixture and the document revision. The test passes when
|
||||
the outputs compare equal over their compared objects and each implementation
|
||||
accepts the other's valid request and reproduces the other's denials. Any
|
||||
divergence traced to an underspecified value is a defect in the specification,
|
||||
not in either implementation, and is fixed there.
|
||||
|
||||
This test has a mandatory negative control. One implementation is patched to
|
||||
emit a denial that differs from the other only outside the compared object —
|
||||
adding a header core does not name, or reordering fields — and the run MUST
|
||||
still pass. A run that fails this control is comparing more than core pins and
|
||||
would reject conforming pairs; the exit test itself is then the defect. The
|
||||
control is retained with the evidence, because a comparison that only ever
|
||||
reports equality proves nothing about what it would have caught.
|
||||
**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 above exists. Until one exists the test cannot be
|
||||
run, and a conformance claim MUST instead 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.
|
||||
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
|
||||
|
||||
@@ -420,19 +308,20 @@ 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;
|
||||
- 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`, so no status witness exists to be stale or revoked, 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
|
||||
`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
|
||||
@@ -440,24 +329,26 @@ one deployed domain does not activate it.
|
||||
|
||||
## Release gate
|
||||
|
||||
Before NIP-FI enforcement or discovery is enabled, reviewers verify that one
|
||||
immutable claim tuple passes every applicable oracle, other than a validly
|
||||
deferred `FI-CONF-INTEROP-EXIT`, at one reviewed revision;
|
||||
that, if the canonical fixture has been published before the review, the claim
|
||||
tuple's exit fixture digest is not `pending-canonical-fixture`;
|
||||
that the protected-ingress inventory has no uncovered or competing authority;
|
||||
that every listed oracle, other than a validly deferred `FI-CONF-INTEROP-EXIT`
|
||||
under **Mutation adequacy**, has a killed, attributed, reachable mutant and every
|
||||
survivor is recorded; that the denial-fixture enumeration is complete for the
|
||||
claimed profiles and its negative control fails as required; that the
|
||||
interoperability exit test has passed against an independent implementation, or
|
||||
is recorded as deferred under **Interoperability exit test** because no
|
||||
independent implementation exists;
|
||||
that every named deployment artifact exists at the claimed deployment revision;
|
||||
and that public and operational sinks pass privacy-canary inspection.
|
||||
Before NIP-FI enforcement or discovery is enabled, reviewers verify, at one
|
||||
reviewed revision, that:
|
||||
|
||||
Documentation review, source review, and static scans are useful review inputs.
|
||||
They close no item in this gate.
|
||||
- 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
|
||||
|
||||
|
||||
@@ -45,7 +45,8 @@ Every trusted edge MUST:
|
||||
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;
|
||||
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
|
||||
@@ -164,16 +165,17 @@ time.
|
||||
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. Any transformation after the protected snapshot is forbidden.
|
||||
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 component outside the protected pre-MAC components,
|
||||
except the independent NIP-98 proof conveyed in the `Authorization` field,
|
||||
whose integrity is protected by its own signed event rather than by this MAC;
|
||||
body interpretation follows the server-resolved body semantics, never
|
||||
unprotected transport metadata such as `Content-Type` or `Content-Encoding`.
|
||||
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
|
||||
|
||||
@@ -367,7 +369,7 @@ Every implementation MUST run these normative negative cases:
|
||||
| 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 transform denies with no replay or authoritative mutation. |
|
||||
| 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. |
|
||||
|
||||
@@ -389,7 +391,7 @@ and these profile traces:
|
||||
| `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. |
|
||||
| `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. |
|
||||
|
||||
|
||||
@@ -173,10 +173,13 @@ 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. `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]
|
||||
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
|
||||
@@ -246,11 +249,6 @@ 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]
|
||||
|
||||
Thus two contract identities do not mean two total version values. Folding a
|
||||
mutable snapshot into policy identity would make benign rotation change policy
|
||||
lineage; omitting it would let evidence under a removed key survive. Stable
|
||||
semantic IDs plus explicit mutable dependency versions preserve both outcomes.
|
||||
|
||||
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:
|
||||
@@ -312,14 +310,14 @@ 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 exact bytes consumed by the
|
||||
application. Absence, duplication, mismatch, validation of only a prefix, or
|
||||
post-validation transformation denies. For an irrelevant body, no
|
||||
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
|
||||
lowercase hexadecimal SHA-256 of the exact body bytes received; duplication or
|
||||
mismatch denies. [FI-TRACE-BODY-BINDING]
|
||||
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
|
||||
@@ -479,7 +477,11 @@ private-posture rule, even `key_mismatch` joins the private-state anonymity set.
|
||||
| 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.
|
||||
For HTTP, the compared denial contract is closed over the status, complete body,
|
||||
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
|
||||
@@ -559,7 +561,7 @@ 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, and ambiguity negative denies. |
|
||||
| `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. |
|
||||
@@ -568,7 +570,7 @@ policy revision. NIP-FI-CONF defines evidence and mutation-adequacy rules.
|
||||
| `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/transformed payload variants deny without effects; a payload tag on an irrelevant-body operation validates identically and denies on duplication or mismatch. |
|
||||
| `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. |
|
||||
@@ -581,7 +583,7 @@ policy revision. NIP-FI-CONF defines evidence and mutation-adequacy rules.
|
||||
| `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; all private-state rows compare byte-identical. |
|
||||
| `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. |
|
||||
@@ -589,6 +591,26 @@ policy revision. NIP-FI-CONF defines evidence and mutation-adequacy rules.
|
||||
| `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
|
||||
@@ -605,4 +627,7 @@ atomically. Availability failures deny rather than degrade to Nostr-only access.
|
||||
- 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