mirror of
https://github.com/block/buzz.git
synced 2026-11-07 01:41:06 +00:00
docs(nip-fi): rewrite NIP-FI as stateless OSS Buzz spec v2 (#7214)
Rewrites `docs/nips/NIP-FI.md` as a stateless spec and deletes the five companion profiles (NIP-FI-CONF, NIP-FI-DELEG, NIP-FI-EDGE, NIP-FI-LIFECYCLE, NIP-FI-MODEL) that presuppose relay-side state. ## What changes ### `docs/nips/NIP-FI.md` — full rewrite The relay-side authority engine (bindings, receipts, lifecycle, invalidation, leases, delegation, enrollment modes, TOFU) is removed. The spec settles exactly five things: 1. **Assertion contract**: JWT claims (`iss`, `sub`, `nostr_pubkey`, `aud` (required), `iat`/`exp`, finite TTL bounds), npub binding semantics, required NIP-42 proof-of-key pairing. Evidence rules (assertion-key-mismatch → `authorization_denied`, freshness) preserved from v1. 2. **Verification**: offline against configured per-issuer JWKS snapshots; multi-issuer; fail-closed on missing/expired/unverifiable. Cites `crates/buzz-auth/src/nip_fi/` (PR 3 / `70895b355`) as implementing the assertion-verification procedure. The `require_attested_key` flag in `IssuerPolicy` is the enforcement primitive for the unconditional `nostr_pubkey` requirement; conformance requires forcing it true for every issuer — follow-on code outside this PR. 3. **Session policy**: required finite `max_connection_lifetime_seconds` knob with no permissive default; re-auth on every reconnect; no in-band renewal. 4. **Admin disconnect API**: session-only normative text with a dedicated `VerifyCommandJwt` procedure — distinct `typ: nip-fi-command+jwt`, signed `method`/`path` binding, normative `maximum_command_age` bound (0 < value ≤ 60 s), atomic `(iss, jti)` reservation as the final admission step after all pure checks. Non-normative note documents cumulative residual access: `max(0, min(exp, iat + maximum_assertion_age) - now)` assuming issuance stops; states that previously issued assertions remain valid for reconnection until their authority expires; states that continued issuance extends access with no protocol-level bound. Non-normative note documents the session-only vs deny-until-TTL trade-off. This verifier and endpoint are follow-on code. 5. **Deletions**: bindings, enrollment, lifecycle, receipts, leases, invalidation, delegation gone. "Out of scope" section names every issuer and deployment concern explicitly. ### Companion profiles deleted NIP-FI-CONF, NIP-FI-DELEG, NIP-FI-EDGE, NIP-FI-LIFECYCLE, NIP-FI-MODEL — all presuppose relay-side binding/lifecycle state. ## What does NOT change The entire `crates/buzz-auth/src/nip_fi/` Rust crate (PR 3 / `70895b355`) implements the assertion-verification procedure described in this spec. No Rust code changes in this PR. The `require_attested_key` enforcement integration and the `VerifyCommandJwt` / disconnect endpoint implementation are explicit follow-on code work. ## Follow-on work (separate PRs after spec merges) - `require_attested_key` forced-true startup enforcement - `VerifyCommandJwt` implementation and disconnect endpoint - Down-migration dropping migrations 0041/0042 (0042 before 0041 per FK order) - `schema.sql` NIP-FI sections removed - `buzz-db` migration test blocks cleaned up --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -1,375 +0,0 @@
|
||||
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.
|
||||
@@ -1,186 +0,0 @@
|
||||
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.
|
||||
@@ -1,417 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,269 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,96 +0,0 @@
|
||||
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.
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user