From 18507ce9f790f1c647854eada853abd72db94078 Mon Sep 17 00:00:00 2001 From: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:20:38 -0500 Subject: [PATCH] docs(nips): add NIP-FI conformance matrix Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com> --- docs/nips/NIP-FI-CONFORMANCE.md | 61 +++++++++++++++++++++++++++++++++ docs/nips/NIP-FI-MODEL.md | 28 ++------------- docs/nips/NIP-FI.md | 2 +- 3 files changed, 65 insertions(+), 26 deletions(-) create mode 100644 docs/nips/NIP-FI-CONFORMANCE.md diff --git a/docs/nips/NIP-FI-CONFORMANCE.md b/docs/nips/NIP-FI-CONFORMANCE.md new file mode 100644 index 0000000000..0e2ba882d1 --- /dev/null +++ b/docs/nips/NIP-FI-CONFORMANCE.md @@ -0,0 +1,61 @@ +# NIP-FI conformance matrix + +This companion matrix makes the normative [NIP-FI specification](NIP-FI.md) and its [formal model](NIP-FI-MODEL.md) reviewable as executable traces. The specification remains normative; the model and this matrix must agree with it. + +Use synthetic issuers, subjects, keys, assertions, and domains. A denial with “no mutation” means no binding, lifecycle, enrollment, membership, publication, or last-seen mutation; a redacted security audit record is allowed. Internal decision names below need not be exposed on the wire: public failures use only the redacted `auth-required:` or `restricted:` classes defined by the specification. + +## State shorthand + +- `B_i`, `B_k`: active binding lookups for the presented identity and proven key. +- `P`: retired exact-pair selector for `(i, k)`. +- `X`: disabled-identity selector for `i`. +- `Y`: revoked-key selector for `k`. +- `Q`: pending explicit replacement for `i`. +- “Lifecycle reads” means `B_i`, `B_k`, `P`, `X`, `Y`, and `Q` are read in the same linearizable transition. + +## Required traces + +| ID | Pre-state / configuration | Presented evidence | Atomic reads | Decision | Mutation | Lease / session effect | Public result | Specification / model anchors | +|---|---|---|---|---|---|---|---|---| +| TR-1 | Trusted-proxy profile; direct verifier ingress is reachable or inbound assertion copies are not stripped | Client-controlled proxy header and otherwise valid proof | None | Reject non-conforming ingress | None | None | `restricted:` or connection rejection without claim detail | [Transport](NIP-FI.md#assertion-transport), [Trust](NIP-FI-MODEL.md#trust-assumptions) | +| TR-2 | Browser WebSocket client without a trusted proxy | Assertion in URL/query string | None | Reject forbidden transport | None | None | No authenticated session; token never logged | [Transport](NIP-FI.md#assertion-transport), [Privacy](NIP-FI.md#privacy) | +| TR-3 | Client-attached HTTP profile | Valid assertion and valid NIP-98 proof on different requests, or malformed/multiple bearer values | None | Reject evidence that is not co-located and unambiguous | None | None | HTTP `401`/`403` with redacted body | [Transport](NIP-FI.md#assertion-transport), [Rejection](NIP-FI.md#rejection-semantics) | +| AS-1 | Accepted issuer configured | Missing assertion, or missing Nostr proof | None | Deny missing independent evidence | None | None | `auth-required:` / HTTP `401` | [Proof](NIP-FI.md#nostr-proof), [Proof model](NIP-FI-MODEL.md#nostr-proof-validity) | +| AS-2 | Accepted issuer configured | Bad signature; symmetric/`none` algorithm; wrong issuer or audience | None | Reject assertion | None | None | `restricted:` / HTTP `403` without validation detail | [Validation](NIP-FI.md#assertion-validation), [Validation model](NIP-FI-MODEL.md#assertion-validity) | +| AS-3 | Accepted issuer configured | Missing/expired `exp`, future `nbf` or `iat` beyond skew | None | Reject assertion | None | None | `restricted:` / HTTP `403` | [Validation](NIP-FI.md#assertion-validation), [Validation model](NIP-FI-MODEL.md#assertion-validity) | +| AS-4 | Accepted issuer configured | Missing, empty, ambiguous, or wrong-type `sub`; malformed or ambiguous key claim | None | Reject assertion | None | None | `restricted:` / HTTP `403` | [Validation](NIP-FI.md#assertion-validation), [Validation model](NIP-FI-MODEL.md#assertion-validity) | +| AS-5 | Key set unavailable, stale beyond policy, or unknown `kid` after a fresh bounded fetch | Otherwise plausible JWT | None | Fail closed; do not drive unbounded refreshes | None | None | `restricted:` / HTTP `403` | [Validation](NIP-FI.md#assertion-validation), [Validation model](NIP-FI-MODEL.md#assertion-validity) | +| AS-6 | Issuers `A` and `B` use the same `sub` | Valid assertion from each issuer | Issuer-qualified lifecycle reads | Treat `(A, sub)` and `(B, sub)` as different identities | Only the selected identity may enroll | Independent leases | Normal result for each qualified identity | [Definitions](NIP-FI.md#definitions), [Domain terms](NIP-FI-MODEL.md#terms-and-domains) | +| PR-1 | Any binding state | Invalid, stale, replayed, wrong-target, or wrong-payload NIP-42/NIP-98 proof | None | Reject proof before authorization | None | None | `restricted:` / HTTP `403` | [Proof](NIP-FI.md#nostr-proof), [Proof model](NIP-FI-MODEL.md#nostr-proof-validity) | +| AU-1 | Any binding state | Valid assertion with `k_a != k` | None | Deny `key_mismatch` before state access | None | None | `restricted:` without either key | [Authorization](NIP-FI.md#authorization), [Transition](NIP-FI-MODEL.md#authorization-and-enrollment-transition) | +| AU-2 | `B_i = B_k = (i, k)` and no lifecycle gate | Matching valid assertion and proof | Lifecycle reads | Allow existing binding | None | Create lease bounded by all applicable limits | Success | [Authorization](NIP-FI.md#authorization), [Transition](NIP-FI-MODEL.md#authorization-and-enrollment-transition) | +| AU-3 | `B_i = (i, k_old)` | Valid assertion for `i`; proof by `k_new` | Lifecycle reads | Deny `binding_conflict` | None | None | `restricted:` without conflict detail | [Authorization](NIP-FI.md#authorization), [Binding invariant](NIP-FI.md#binding-invariant) | +| AU-4 | `B_k = (i_other, k)` | Valid assertion for `i`; proof by `k` | Lifecycle reads | Deny `binding_conflict` | None | None | `restricted:` without conflict detail | [Authorization](NIP-FI.md#authorization), [Binding invariant](NIP-FI-MODEL.md#binding-invariant) | +| EN-1 | No binding or lifecycle gate; `attested-key` mode | Valid assertion with `k_a = k`; valid proof | Lifecycle reads | Allow created binding | Create `(i, k)` with `attested-key` provenance | Create bounded lease | Success | [Enrollment](NIP-FI.md#enrollment-modes), [Transition](NIP-FI-MODEL.md#authorization-and-enrollment-transition) | +| EN-2 | No binding or lifecycle gate; `attested-key` mode | Valid assertion without key claim; valid proof | Lifecycle reads | Deny `key_attestation_required` | None | None | `restricted:` | [Enrollment](NIP-FI.md#enrollment-modes), [Transition](NIP-FI-MODEL.md#authorization-and-enrollment-transition) | +| EN-3 | No active binding; `provisioned` mode | Valid assertion and proof | Lifecycle reads | Deny `binding_required` | None | None | `restricted:` | [Enrollment](NIP-FI.md#enrollment-modes), [Transition](NIP-FI-MODEL.md#authorization-and-enrollment-transition) | +| EN-4 | No binding or lifecycle gate; `tofu` mode | Valid assertion without key claim; valid proof | Lifecycle reads | Allow created binding with accepted TOFU risk | Create `(i, k)` with `tofu` provenance | Create bounded lease | Success | [Enrollment](NIP-FI.md#enrollment-modes), [Transition](NIP-FI-MODEL.md#authorization-and-enrollment-transition) | +| EN-5 | No binding or lifecycle gate; `tofu` mode | Valid assertion with `k_a = k`; valid proof | Lifecycle reads | Allow created binding | Create `(i, k)` with `attested-key` provenance; later requests cannot downgrade it | Create bounded lease | Success | [Enrollment](NIP-FI.md#enrollment-modes), [Transition](NIP-FI-MODEL.md#authorization-and-enrollment-transition) | +| CC-1 | No binding or lifecycle gate | Concurrent identical first use of `(i, k)` | Serialized lifecycle reads | One create; all others observe exact match and allow existing | Exactly one insertion | Equivalent bounded leases | Success | [Authorization](NIP-FI.md#authorization), [Transition](NIP-FI-MODEL.md#authorization-and-enrollment-transition) | +| CC-2 | No binding or lifecycle gate | Concurrent `(i, k1)` / `(i, k2)` or `(i1, k)` / `(i2, k)` | Serialized lifecycle reads | At most one create; conflicting attempt denies | At most one insertion | Lease only for committed pair | Conflicting request receives `restricted:` | [Authorization](NIP-FI.md#authorization), [Binding invariant](NIP-FI-MODEL.md#binding-invariant) | +| CC-3 | Any prospective enrollment | Lifecycle lookup, insertion, or committed-result reread fails | Required state is unavailable | Fail closed | None beyond a transaction proven committed | No new lease from an unreadable result | `restricted:` | [Authorization](NIP-FI.md#authorization), [Safety](NIP-FI-MODEL.md#safety-properties) | +| LC-1 | `(i, k) in P`; no active binding | Still-valid matching assertion and proof, with or without matching key claim | Lifecycle reads | Deny `pair_retired` | None | No lease | `restricted:` without tombstone detail | [Revocation](NIP-FI.md#revocation-and-rotation), [Lifecycle model](NIP-FI-MODEL.md#revocation-and-rotation) | +| LC-2 | `i in X`, including before first enrollment | Valid assertion for `i`; proof by any key | Lifecycle reads | Deny `identity_disabled` | None | Invalidate/deny direct and dependent delegated leases | `restricted:` | [Revocation](NIP-FI.md#revocation-and-rotation), [Lifecycle model](NIP-FI-MODEL.md#revocation-and-rotation) | +| LC-3 | `k in Y`, including without an active binding | Valid assertion for any identity; proof by `k` | Lifecycle reads | Deny `key_revoked` | None | Invalidate/deny every lease depending on `k` | `restricted:` | [Revocation](NIP-FI.md#revocation-and-rotation), [Lifecycle model](NIP-FI-MODEL.md#revocation-and-rotation) | +| LC-4 | `Q(i) = k_old`; no active binding | Valid assertion for `i`; proof by fresh `k_new` | Lifecycle reads | Deny `explicit_replacement_required` | None | No lease | `restricted:` | [Revocation](NIP-FI.md#revocation-and-rotation), [Lifecycle model](NIP-FI-MODEL.md#revocation-and-rotation) | +| LC-5 | Active old pair or `Q(i) = k_old`; replacement is lifecycle-eligible | Separately authorized rotation/recovery; fresh `k_a = k_new` when required | Old/new active and lifecycle state | Allow privileged replacement | Atomically retire old pair/key, create new pair, clear `Q`, retain history | Invalidate old direct/delegated leases; new pair may obtain a bounded lease | Operator transition; not an `Authorize` response | [Revocation](NIP-FI.md#revocation-and-rotation), [Lifecycle model](NIP-FI-MODEL.md#revocation-and-rotation) | +| LC-6 | Authorization and retire/disable/revoke/replace race | Valid evidence plus concurrent lifecycle transition | Serialized active and lifecycle state | Serialization decides; completed lifecycle state cannot be violated | No post-transition resurrection or partial replacement | Matching leases invalidated when transition is observed | Success only for the serialization that precedes the lifecycle transition | [Authorization](NIP-FI.md#authorization), [Safety](NIP-FI-MODEL.md#safety-properties) | +| SE-1 | HTTP endpoint | Valid assertion and NIP-98 proof | Lifecycle reads | Apply decision to this request only | Enrollment only when allowed | No reusable HTTP lease implied | Per-request result | [Sessions](NIP-FI.md#session-semantics), [Session model](NIP-FI-MODEL.md#session-behavior) | +| SE-2 | Active WebSocket lease reaches its earliest bound | No new upgrade assertion | Current time and observed dependencies | Reject protected operations or close | None | Lease expires; renewal requires a new connection, fresh assertion, and fresh NIP-42 proof | `restricted:` / closure without private detail | [Sessions](NIP-FI.md#session-semantics), [Session model](NIP-FI-MODEL.md#session-behavior) | +| SE-3 | One WebSocket authenticates `k1` and `k2`; only `k1` is authorized | Operations attributed to each key | Per-key lease lookup | Allow only operations attributed to `k1` | None | `k1` lease never authorizes `k2` | Independent per-key result | [Sessions](NIP-FI.md#session-semantics), [Session model](NIP-FI-MODEL.md#session-behavior) | +| SE-4 | Direct or delegated lease depends on binding/policy/delegation state that becomes invalid | Previously valid session | Observed dependency state | Deny after the documented detection bound | None | Invalidate every dependent lease; do not claim immediate revocation when polling | Redacted denial/closure | [Sessions](NIP-FI.md#session-semantics), [Safety](NIP-FI-MODEL.md#safety-properties) | +| DG-1 | Owner has an eligible active binding; delegation is valid and narrower | Delegate proves its own key; no delegate assertion | Owner binding/lease plus delegation conditions | Allow only intersected delegated operations | Never create owner's federated binding for delegate | Lease bounded by owner, delegation, policy, and implementation limits | Success for allowed delegated operation | [Delegation](NIP-FI.md#delegation), [Delegation model](NIP-FI-MODEL.md#delegation) | +| DG-2 | Owner binding is retired/revoked, or delegation expired/revoked | Delegate presents formerly valid proof | Owner and delegation dependencies | Deny | None | Invalidate dependent delegated lease on documented detection schedule | `restricted:` | [Delegation](NIP-FI.md#delegation), [Delegation model](NIP-FI-MODEL.md#delegation) | +| BD-1 | Two server-resolved authorization domains | Client attempts to choose another domain or reuse evidence across domains | State only for trusted resolved `D` | Ignore/reject client domain choice; no cross-domain authority | None outside selected domain | Domain-scoped lease only | Redacted denial when no selected-domain authority exists | [Definitions](NIP-FI.md#definitions), [Safety](NIP-FI-MODEL.md#safety-properties) | +| OP-1 | Conflict, validation failure, or lifecycle denial | Any denied request | As required by the decision | Deny without disclosing the conflicting identity, key, claims, or selector | None | None or matching lease invalidation | Only `auth-required:` / `restricted:` or HTTP `401`/`403` | [Rejection](NIP-FI.md#rejection-semantics), [Privacy](NIP-FI.md#privacy) | +| OP-2 | Relay supports a subset of transports, enrollment modes, or delegation | NIP-11 query | None | Advertise only supported behavior | None | None | No issuer-internal tenant, claim, or audience detail | [Discovery](NIP-FI.md#discovery), [Conformance hooks](NIP-FI-MODEL.md#conformance-hooks-for-the-nip) | +| OP-3 | Separate opt-in relay-signed public projection exists | Approved label, or a forged/public projection presented as auth evidence | Normal NIP-FI evidence and lifecycle state only | Publish only approved non-private label; never accept projection as authorization evidence | Projection is separate from binding state | No authorization lease from projection | No `iss`, `sub`, bearer material, or private claim disclosure | [Privacy](NIP-FI.md#privacy), [Safety](NIP-FI-MODEL.md#safety-properties) | + +## Conformance claim + +An implementation may claim NIP-FI conformance only for the transport profiles, enrollment modes, delegation behavior, and optional projection behavior it advertises and tests. Every applicable trace above must pass against the same reviewed implementation revision. Unsupported optional behavior must be omitted from discovery rather than represented as implemented. diff --git a/docs/nips/NIP-FI-MODEL.md b/docs/nips/NIP-FI-MODEL.md index 046f7cf3e0..ed643f04a7 100644 --- a/docs/nips/NIP-FI-MODEL.md +++ b/docs/nips/NIP-FI-MODEL.md @@ -239,32 +239,9 @@ Assuming the issuer, key source, binding store, and network are available: No authorization liveness is promised while identity disablement, key revocation, pair retirement, or pending replacement blocks a request. Liveness is also intentionally not guaranteed during issuer/JWKS/storage outage; availability must not override identity safety. -# Representative attack traces +# Conformance traces -| Trace | Required result | -|---|---| -| Valid assertion for `i`, attacker proves unbound `k_x`, `i` already bound to `k_v` | Deny `binding_conflict` | -| Valid assertion with key claim `k_v`, attacker proves `k_x` | Deny `key_mismatch` before mutation | -| Stolen assertion for never-enrolled `i`, attacker proves `k_x` | Deny in `attested-key`/`provisioned`; TOFU can bind and explicitly accepts this risk | -| Client injects trusted-proxy header while bypassing proxy | Deployment is non-conforming; verifier must reject direct/untrusted ingress | -| Assertion for issuer `A`, same `sub` as issuer `B` | Distinct identities; never collide or inherit binding | -| Assertion has wrong audience, expired `exp`, unknown algorithm/key, malformed subject/key | Deny without binding mutation | -| Concurrent first use of `(i,k1)` and `(i,k2)` | At most one commits; the other denies conflict | -| Reuse of valid WebSocket authorization after assertion expiry | Deny protected operation or reauthenticate/close | -| Retire `(i,k)`, then replay a matching assertion and proof in TOFU | Deny `pair_retired` without mutation | -| Retire `(i,k)`, then replay an issuer key claim matching `k` | Deny `pair_retired` without mutation | -| Disable never-enrolled `i`, then present any valid assertion and proof | Deny `identity_disabled` without mutation | -| Revoke `k`, then present it for another identity | Deny `key_revoked` without mutation | -| Revoke active `k`, then present fresh `k_new` for the same identity | Deny `explicit_replacement_required`; require privileged replacement | -| `Authorize` races pair retirement or key revocation | Serialize; no binding that violates the completed transition survives | -| Rotate to an active, revoked, or previously retired replacement | Deny without partial mutation | -| Two concurrent replacements for one identity | At most one commits; the other denies after observing committed state | -| Successful explicit replacement | Old pair and key remain denied; new active pair authorizes | -| Lifecycle-state lookup fails | Deny without enrollment mutation | -| New key presented for bound identity | Deny; require explicit rotation | -| Display name/email changes while `(iss,sub)` is stable | May update metadata; binding identity is unchanged | -| One NIP-42 connection authenticates `k1` and `k2`, only `k1` is bound | Only operations attributed to `k1` receive its lease | -| JWT or corporate identifier is accidentally published as event/tag | Non-conforming privacy failure; assertion transport must not enter relay event history | +The companion [NIP-FI conformance matrix](NIP-FI-CONFORMANCE.md) defines stable-ID success, denial, concurrency, lifecycle, session, delegation, disclosure, and privacy traces. In particular, its revoked-pair replay trace requires a stable denial with no mutation even when the still-valid assertion carries a matching key claim; only the separately authorized replacement transition can restore authority. # Conformance hooks for the NIP @@ -287,4 +264,5 @@ It should not standardize database schema, lock mechanism, Okta-specific claims, - NIP-05 issuer-controlled identifier mapping precedent: https://github.com/nostr-protocol/nips/blob/8f8444d05a8842c40211ded5d10af3521541f865/05.md - NIP-46 external auth challenge precedent: https://github.com/nostr-protocol/nips/blob/8f8444d05a8842c40211ded5d10af3521541f865/46.md - Companion protocol specification: [`NIP-FI.md`](NIP-FI.md) +- Companion conformance matrix: [`NIP-FI-CONFORMANCE.md`](NIP-FI-CONFORMANCE.md) - Buzz PR #1476 at `1e9822de8dbe0ae91c00c0ce0ed8ff583915692f` is a disabled partial foundation, not a complete NIP-FI implementation; future-`iat`, discovery, lifecycle, and lease conformance remain additive work. diff --git a/docs/nips/NIP-FI.md b/docs/nips/NIP-FI.md index 17ee7c35e6..783557c562 100644 --- a/docs/nips/NIP-FI.md +++ b/docs/nips/NIP-FI.md @@ -182,7 +182,7 @@ A separate, opt-in relay-signed projection protocol such as NIP-85 MAY publish a - **Availability vs. safety**: issuer, key-set, and storage outages deny. Availability MUST NOT override identity safety. - **Cross-issuer collision**: identical `sub` values under different issuers are distinct identities and MUST never collide or inherit each other's bindings. -A companion formal model of this protocol — state machine, safety and liveness properties, and attack traces — accompanies this specification. +The companion [formal model](NIP-FI-MODEL.md) defines the state machine and safety/liveness properties. The [conformance matrix](NIP-FI-CONFORMANCE.md) supplies stable, reviewable success, denial, concurrency, lifecycle, session, disclosure, and privacy traces. ## Implementation relationship