diff --git a/.env.example b/.env.example index 696d3a06175..34838b74474 100644 --- a/.env.example +++ b/.env.example @@ -51,6 +51,17 @@ RELAY_URL=ws://localhost:3000 # (use `just web` for Vite HMR instead). # BUZZ_WEB_DIR=./web/dist +# ----------------------------------------------------------------------------- +# Federated identity (NIP-FI) +# ----------------------------------------------------------------------------- +# This documentation stack defines the future NIP-FI contract but does not add +# or activate its runtime parser. Do not infer support for +# BUZZ_NIP_FI_V1_CONFIG_JSON or any operating mode from this example file. +# The proposed document shape and activation gates are recorded in +# docs/CORPORATE_IDENTITY.md and docs/NIP_FI_DEPLOYMENT.md for the later +# implementation stack. NIP-FI discovery and enforcement remain off until +# that exact implementation and deployment pass the complete evidence matrix. + # ----------------------------------------------------------------------------- # Git (NIP-34 bare repositories) # ----------------------------------------------------------------------------- diff --git a/README.md b/README.md index 07523f1a9bd..dce180c52b0 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,7 @@ A Rust workspace of focused crates. Single source of truth: the relay. See [ARCH - **[VISION.md](VISION.md)** · **[VISION_SOVEREIGN.md](VISION_SOVEREIGN.md)** · **[VISION_PROJECTS.md](VISION_PROJECTS.md)** · **[VISION_AGENT.md](VISION_AGENT.md)** — the four vision docs - **[ARCHITECTURE.md](ARCHITECTURE.md)** — system design, kind ranges, subsystem boundaries +- **[NIP-FI](docs/nips/NIP-FI.md)** · **[integration](docs/NIP_FI_INTEGRATION.md)** · **[threat model](docs/NIP_FI_THREAT_MODEL.md)** · **[deployment](docs/NIP_FI_DEPLOYMENT.md)** · **[operations](docs/NIP_FI_RUNTIME_OPERATIONS.md)** — federated-identity contract and public guidance; this documentation revision does not include or activate a runtime adapter - **[TESTING.md](TESTING.md)** — multi-agent E2E test suite - **[CONTRIBUTING.md](CONTRIBUTING.md)** · **[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)** · **[SECURITY.md](SECURITY.md)** · **[GOVERNANCE.md](GOVERNANCE.md)** diff --git a/SECURITY.md b/SECURITY.md index 96222e029d0..b45ca8893f8 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -53,17 +53,36 @@ REST endpoints authenticate via the client signs a `kind:27235` event containing the request URL and method. The relay verifies the Schnorr signature and extracts the pubkey. -### Authorization — Channel Membership as the Gate +### Authorization and admission -Channel membership is the **only** access control mechanism. There are no -separate ACL lists or capability taxonomies. If a principal (human or agent) -is a member of a channel, they can read and write to it. If they are not a -member, the relay rejects their requests — even if they are authenticated. +In the current merged runtime, channel membership is Buzz's only +application-level access-control mechanism. If a principal (human or agent) +is a member of a channel, application policy may allow them to read and write +it. If they are not a member, the relay rejects their requests even if they +are authenticated. Private channels are invisible to non-members: they do not appear in channel listings, and subscription filters for private channel events return nothing unless the subscriber is a member. +[NIP-FI](docs/nips/NIP-FI.md) defines a future optional additional admission +authority that combines provider-neutral issuer assertions with fresh Nostr +key proof, current durable binding and lifecycle state, and final application +admission. When enabled for a domain, one current NIP-FI authority covers every +protected ingress atomically, and both NIP-FI and application authorization +must allow each operation. NIP-FI does not replace NIP-42, NIP-98, Nostr +signatures, channel membership, or resource policy. + +That NIP-FI behavior is not shipped in the current merged runtime. This +documentation revision does not include or activate a NIP-FI runtime adapter. +A later exact-head implementation and deployment must pass the +[behavioral evidence matrix](docs/nips/NIP-FI-CONFORMANCE.md) before publishing +discovery or enforcing the contract. Source scans, prose, and configuration +presence do not establish conformance. See the [threat +model](docs/NIP_FI_THREAT_MODEL.md), [stock +deployment](docs/NIP_FI_DEPLOYMENT.md), and [runtime +operations](docs/NIP_FI_RUNTIME_OPERATIONS.md). + ### Append-Only Audit Log All events are written to a tamper-evident audit log (`buzz-audit`). Each diff --git a/deploy/charts/buzz/README.md b/deploy/charts/buzz/README.md index 010b3dffdee..cf3089aeca1 100644 --- a/deploy/charts/buzz/README.md +++ b/deploy/charts/buzz/README.md @@ -78,6 +78,22 @@ Save these. Losing any of them is data loss. See NOTES.txt printed by `helm inst 4. Git PVC — repo on-disk state served by the relay's git endpoint. 5. Owner private key — held by the operator, not by this chart. Restore by re-installing with the same `ownerPubkey`. +## NIP-FI readiness + +This chart does not provision a NIP-FI runtime, trusted-edge topology, issuer +integration, secret keys, or conformance runner. It makes no claim that the +proposed `BUZZ_NIP_FI_V1_CONFIG_JSON` document is parsed or enforced. Do not +advertise or enforce NIP-FI from this chart, and do not use an ingress identity +header or provider-specific sidecar as a fallback authority. + +An activating deployment must pin an exact image, keep policy separate from +secret values, isolate verifier ingress for `trusted-proxy-hmac-v2`, include +every fail-closed dependency in readiness, and link an immutable exact-head +behavioral report. A rendered chart and healthy pod do not prove those +behaviors. See the +[provider-neutral deployment guide](../../../docs/NIP_FI_DEPLOYMENT.md) and +[runtime operations guide](../../../docs/NIP_FI_RUNTIME_OPERATIONS.md). + ## Honest limitations (v1) - **Bundled MinIO is eval-only.** The quickstart profile runs an in-cluster diff --git a/deploy/compose/README.md b/deploy/compose/README.md index 0de524fb5b5..7fdec820337 100644 --- a/deploy/compose/README.md +++ b/deploy/compose/README.md @@ -36,11 +36,30 @@ keypair. `buzz-admin migrate` before starting the relay when bootstrapping a fresh database. Auto-migration requires an image that includes embedded SQLx migrations. +- The proposed NIP-FI configuration contract is future-facing; this bundle + does not imply that the current relay parses or enforces it. See the + [identity configuration contract](../../docs/CORPORATE_IDENTITY.md). - The stack uses Postgres, Redis, MinIO, and a git data volume because those are real Buzz dependencies today. Minimal mode can simplify this later. Run `./run.sh backup-hint` for the backup checklist. +## NIP-FI readiness + +This Compose bundle does not provision a NIP-FI runtime, trusted edge, issuer +integration, or conformance runner. It makes no claim that the proposed +`BUZZ_NIP_FI_V1_CONFIG_JSON` document is parsed or enforced. Do not advertise +or enforce NIP-FI from this bundle, and do not add a provider-specific sidecar +or unsigned corporate identity header as a substitute. + +An activating deployment must pin an exact image, isolate verifier ingress +when `trusted-proxy-hmac-v2` is enabled, deliver HMAC secrets through a +secret store rather than `.env`, and pass the complete exact-head behavioral +matrix before activation. A valid Compose render or healthy relay does not +close those gates. See the +[provider-neutral deployment guide](../../docs/NIP_FI_DEPLOYMENT.md) and +[runtime operations guide](../../docs/NIP_FI_RUNTIME_OPERATIONS.md). + ## Validation Before sharing an install link publicly, verify a fresh install with: diff --git a/docs/CORPORATE_IDENTITY.md b/docs/CORPORATE_IDENTITY.md new file mode 100644 index 00000000000..38a7f5b0c0f --- /dev/null +++ b/docs/CORPORATE_IDENTITY.md @@ -0,0 +1,241 @@ +# Future corporate identity configuration contract + +Historical corporate identity approaches based on provider-specific middleware, unsigned forwarded headers, or a parallel authorization authority are retired guidance. Buzz's provider-neutral contract is [NIP-FI](nips/NIP-FI.md). + +## Availability boundary + +This documentation revision defines a proposed configuration contract for a +later NIP-FI implementation. It does not add a parser for +`BUZZ_NIP_FI_V1_CONFIG_JSON`, remove or reject legacy variables, install a +protected authorization runtime, or activate discovery or enforcement. The +later implementation stack must reconcile this shape with its reviewed code +and executable adapters before claiming any behavior below. + +Even after the parser exists, configuring the runtime is not conformance. A deployment must pass the +[behavioral evidence matrix](nips/NIP-FI-CONFORMANCE.md) at its exact +deployed revision before NIP-FI discovery or enforcement is advertised, and +operators must verify the effective configuration in their deployment; +public wording alone is not activation or conformance evidence. + +## Proposed runtime document + +The later implementation stack reserves `BUZZ_NIP_FI_V1_CONFIG_JSON` as its +sole identity-configuration input. `V1` is the reserved input name, while the +document's `configuration_contract_revision` records breaking shape changes; +neither names or enables a legacy transport profile. The implementation must +provide the following modes and reject the legacy provider-specific variables +rather than treating them as aliases. + +### Proposed operating modes + +- **Off:** `BUZZ_NIP_FI_V1_CONFIG_JSON` is unset. Protected composition is + not installed and existing unprotected behavior remains available. +- **DenyProtected:** the document is `{"deny_protected":true}`. Every + protected route is denied before its handler runs. This mode accepts no + other field; an additional field fails startup. +- **Enforce:** `deny_protected` is absent or false and the complete document + below is provided. Missing, unknown, empty, duplicate, contradictory, or + invalid fields fail startup. + +```json +{ + "configuration_contract_revision": 2, + "deny_protected": false, + "domains": [ + { + "id": "community.example.test", + "authorization_domain_id": "00000000-0000-0000-0000-000000000001", + "authorities": ["community.example.test:443"], + "issuers": [ + { + "issuer": "https://issuer.example.test", + "audiences": ["community.example.test"], + "subject_claim": "sub", + "event_author_claim": "nostr_pubkey", + "clock_skew_seconds": 30, + "maximum_token_lifetime_seconds": 3600, + "jwks": { + "jwks_uri": "https://issuer.example.test/.well-known/jwks.json" + } + } + ], + "lease": { "maximum_seconds": 300 }, + "policy_revision": 1, + "audit": { + "max_events_per_domain": 1000000, + "max_bytes_per_domain": 4294967296, + "max_envelope_bytes": 65536 + }, + "denial_observation": { + "maximum_records": 100000, + "maximum_bytes": 268435456, + "maximum_record_bytes": 1024 + }, + "transport": { + "kind": "trusted_proxy_hmac_v2", + "active_secrets_base64url": ["REPLACE_WITH_BASE64URL_SECRET"], + "maximum_provenance_age_seconds": 60, + "future_skew_seconds": 5 + }, + "enrollment": { "kind": "attested-key" }, + "delegation": { "enabled": false } + } + ] +} +``` + +Revision `2` is an independently reviewed, breaking documentation-first +revision from the earlier flat draft. It does not claim compatibility with a +current parser. The later implementation must update its parser, validation, +fixtures, policy digests, and adapters before claiming this contract. + +### Proposed document rules and bounds + +The future parser must reject unknown and duplicate fields. In Enforce mode: + +- `configuration_contract_revision` is the unsigned integer `2` and + `domains` is non-empty. +- Domain `id`, `authorization_domain_id`, and every authority are non-empty + and unique across the document. Each domain's authority list is non-empty + and contains no duplicates. +- `authorization_domain_id` is the canonical lowercase UUID text form. Remove + its hyphens and decode the hexadecimal octets from left to right to obtain + the exact 16 opaque bytes used by HMAC-v2; any other spelling or length + fails startup. +- Each authority is the server-configured lowercase ASCII host with an + explicit decimal effective port and brackets around IPv6, matching the + HMAC-v2 canonical input. Non-canonical or ambiguous authority text fails + startup. +- Each domain has at least one issuer. Issuer identifiers are unique within a + domain. Each `issuer` and audience is a non-empty string of at most 2048 + characters, and each issuer's audience list is non-empty and unique. +- The implementation's closed asymmetric algorithm set enters verifier-policy + identity through deterministic policy construction. If a later document + makes that set configurable, adding or removing an algorithm must change the + identity and algorithm order must be normalized. +- Each issuer's `subject_claim` defaults to `sub`. It and the optional `event_author_claim` + are limited to 128 characters. +- Each issuer's `clock_skew_seconds` defaults to `0` and is at most 300. +- Each issuer's `maximum_token_lifetime_seconds` is required, positive, and at most 86400. +- Each issuer's `jwks` accepts exactly one HTTPS `jwks_uri` or `discovery_uri`; credentials, + fragments, redirects, and private-network targets are rejected. The source + kind and normalized authenticated URI enter verifier-policy identity. +- Each domain's `lease.maximum_seconds` is required, positive, and at most + 3600. Detection bounds derive from this lease limit, immediate version + fencing, and bounded post-commit invalidation behavior. Presentation is + non-authoritative. +- Each domain's `policy_revision` is required and positive. +- Each domain's `audit` sets the immutable authorization-evidence capacity + (`max_events_per_domain`, `max_bytes_per_domain`, `max_envelope_bytes`). + Values are positive and finite, validation is overflow safe, and + `max_envelope_bytes` cannot exceed `max_bytes_per_domain`. There is no online + prune, export, reset, acknowledgement, or recovery workflow. Successful or + authorization-affecting operations consume this finite budget; denied + operations do not consume it and never create authorization receipts. + Legitimate exhaustion is an accepted, unrecoverable, domain-wide fail-closed + outage within the installation and domain lineage. Sizing is an irreversible + installation-lifetime decision, so operators monitor consumption and alert + with substantial headroom. See the threat model's [availability and resource + exhaustion](NIP_FI_THREAT_MODEL.md#availability-and-resource-exhaustion) + analysis. +- Each domain's `denial_observation` has positive finite `maximum_records`, + `maximum_bytes`, and `maximum_record_bytes`. Validation is overflow safe and + `maximum_record_bytes` cannot exceed `maximum_bytes`. This capacity is + independent from authorization audit. Exhaustion permits drop or bounded + truncation only and emits aggregate saturation signals where possible; it + cannot weaken, delay, retry, reverse, receipt, bind, replay, or otherwise + mutate a denial. +- `transport` and `enrollment` are required non-empty domain objects consumed + by matching runtime adapters. `enrollment.kind` is exactly `attested-key`, + `provisioned`, or `tofu`; `delegation` remains optional and disabled by + omission as described below. +- `transport.kind` is `client_attached`, `trusted_proxy_hmac_v2`, or a private + registered identifier matching `x---v` and accepted + only when its deployment-installed adapter and profile contract satisfy + NIP-FI. The example remains stock. Selection occurs before listeners open + for each bound route and domain, never from request input, and failure never + falls back to another profile. + +Issuer-level fields own issuer, audience, claim, time, and JWKS semantics. +Domain-level fields own transport, enrollment, lease, policy revision, audit +capacity, denial-observation capacity, and delegation. Stable verifier policy +identity covers every semantic input, authenticated key-source identity, and +the compiled verifier-contract fingerprint. + +The future implementation must validate each `jwks` refresh policy at construction: a fetched document +cannot exceed 4 MiB, a snapshot cannot stay fresh longer than 24 hours, every +refresh bound is finite and nonzero, and an accepted key set contains between +1 and 128 keys. Production deployments may use tighter bounds. Key refreshes are +single-flight and stale verification fails closed. + +### Delegation + +Delegation is disabled by default. Omitting the `delegation` object or setting +`{"enabled": false}` disables it, and a disabled delegation object must not +carry capabilities or a lifetime. Enabling it requires `enabled: true`, a +non-empty unique `capabilities` list drawn from the closed route-capability +set, and a positive `maximum_seconds` of at most 3600 that does not exceed +`lease.maximum_seconds`. Delegated authority is always capability-scoped — +never transport-wide — and the relay's NIP-11 information document does not +advertise delegation. + +Corporate-identity delegation is unsupported by this documentation stack. The public production +path does not implement complete delegated issuance, owner-bound resolution, +expiry, invalidation, reconnect, or protected-transport behavior. Operators +must keep delegation disabled, and discovery must report it as false or omit +it. Enabling the configuration shape does not add the missing authority and +must not be used to advertise support. See the +[integration guide's availability boundary](NIP_FI_INTEGRATION.md#delegation-availability) +for the implementation and evidence requirements. + +Direct NIP-FI authorization and future delegation are separate capabilities. +Delegation can be advertised only after a reviewed delegated-owner +implementation passes the complete applicable cross-transport lifecycle and +session evidence. + +## Supported boundary + +NIP-FI combines a verified issuer-qualified assertion result with independent fresh Nostr key proof, current durable binding and lifecycle state, server-owned request context, and final application admission. The stock `trusted-proxy-hmac-v2` construction binds HMAC provenance to the complete canonical request. A private registered trusted-edge profile may use another reviewed construction only when it satisfies the same normalized-result and final-admission contract. Header presence and network location alone are insufficient. + +NIP-FI defines no public corporate directory or identity projection. Issuer-qualified identity and profile claims remain access-controlled enforcement data. + +## No parallel authority + +An enforcing domain has one current NIP-FI authority and policy lineage for every protected ingress. Do not: + +- accept a legacy corporate header when NIP-FI denies; +- keep a provider-specific identity path for selected routes; +- infer identity from email or subject without the configured issuer; +- copy assertion expiry into durable binding expiry; +- import revoked, disabled, or retired state as an active binding; or +- expose corporate claims in Nostr events, discovery, logs, metrics, or traces. + +NIP-42 and NIP-98 continue to prove control of a Nostr key. They do not replace the additional NIP-FI authority for a protected operation. + +## Migration plan + +1. **Inventory:** enumerate every protected WebSocket and HTTP ingress, existing identity source, forwarded field, policy, binding store, lifecycle action, and fallback. +2. **Freeze legacy expansion:** add no routes, providers, claims, or identities to the legacy authority while migration is in progress. +3. **Define domains and policies:** map each server-owned domain to exact issuer-qualified identities `(iss, sub)`, accepted semantics, enrollment mode, and transport profile. +4. **Normalize state:** represent active durable bindings, immutable provenance, retired pairs, disabled identities, revoked keys, pending replacement lineage, typed history, and versions. +5. **Verify imports:** require independent evidence for imported identity/key pairs. Do not treat a forwarded header, email match, or expired assertion as proof of key control. +6. **Install without activation:** deploy the later exact implementation with discovery and enforcement off, one canonical normalized-result and final-admission contract, and no legacy fallback. +7. **Run behavior:** execute all applicable `FI-TRACE-*` adapters at the exact artifact, deployment, and policy digests, including route inventory and deployed-boundary proxy negatives. +8. **Cut over atomically:** enable one authority across the complete protected-ingress set and remove the legacy path. Canary isolated domains or deployments, not individual routes under competing authorities. +9. **Verify and retain:** test old headers, old keys, tombstones, conflicts, denial privacy, dependency outages, restore, and rollback; retain privacy-safe evidence. + +If historical data lacks proof or unambiguous issuer qualification, keep it non-authoritative until a separately authorized provisioning or recovery transition establishes current state. + +## Rollback and repair + +Rollback returns to a previously conformant artifact with compatible current state, or disables discovery and fails protected operations closed. It never restores unsigned forwarding, a removed verification key, a parallel provider runtime, or an older authority database. + +Correct an imported binding or lifecycle fact with a reviewed privileged compensating transition. Preserve typed history and incident evidence; do not delete the record to make ordinary enrollment available again. + +## Operator references + +- [Integration contract](NIP_FI_INTEGRATION.md) +- [Threat model](NIP_FI_THREAT_MODEL.md) +- [Stock deployment](NIP_FI_DEPLOYMENT.md) +- [Runtime operations](NIP_FI_RUNTIME_OPERATIONS.md) +- [Contributor guide](NIP_FI_CONTRIBUTING.md) diff --git a/docs/NIP_FI_CONTRIBUTING.md b/docs/NIP_FI_CONTRIBUTING.md new file mode 100644 index 00000000000..5191236fae9 --- /dev/null +++ b/docs/NIP_FI_CONTRIBUTING.md @@ -0,0 +1,157 @@ +# Contributing to NIP-FI + +This guide applies to public NIP-FI protocol, implementation, deployment, and operations changes in Buzz. Repository-wide rules in [CONTRIBUTING.md](../CONTRIBUTING.md) still apply. + +## Start with the contract + +Read these together: + +1. [NIP-FI](nips/NIP-FI.md) defines normative behavior. +2. [The formal model](nips/NIP-FI-MODEL.md) defines state, invariants, and transitions. +3. [The conformance evidence matrix](nips/NIP-FI-CONFORMANCE.md) defines the behavioral release gate. +4. [The threat model](NIP_FI_THREAT_MODEL.md) records assets, trust boundaries, and residual risk. + +Non-normative implementation or operator prose cannot weaken those documents. + +## Change ownership + +Keep changes in the layer that owns them: + +- normative semantics belong in the specification and model; +- stable behavioral oracles belong in the conformance matrix; +- runtime adapters, state, routes, and tests belong in the implementation stack; +- network isolation and secret delivery belong in deployment artifacts; +- lifecycle procedures and evidence retention belong in operations guidance; and +- public entry points link to accepted behavior without claiming unexecuted support. + +Do not move later runtime behavior into an earlier reviewed commit to make a checklist appear complete. Preserve protected ancestry and append reviewable, signed-off commits. + +## Stable labels + +`FI-INV-01` through `FI-INV-16` and the 24 `FI-TRACE-*` identifiers are public review interfaces. Do not renumber them. When an existing profile-neutral trace gains a new transport construction, record an explicit transport-contract revision and profile-contract digest; evidence under the older meaning is not transferable. + +When behavior changes: + +1. update the normative text and model together; +2. decide whether an existing trace still represents the same oracle or needs an explicit contract revision; +3. add a new stable trace only when no existing trace can express the behavior; +4. update the matrix and example report in the same series; and +5. hand the label to the implementation stack for executable adapter coverage. + +A wording cleanup that does not change behavior should not churn labels. + +## Behavioral adapters + +An adapter maps a stable trace to executable behavior at one exact implementation revision. It records the command, test IDs, fixtures, expected oracle, artifacts, cleanup, transport-contract revision, and applicable profile-contract digest. + +Adapters should exercise production or production-equivalent entry points. Internal helpers may inspect state or inject an outage. They cannot replace the protected operation under test. + +Examples: + +- verifier parity runs one shared authorization-projection corpus through every transport adapter, validates each profile's identity, revision, digest, `policy_id`, and deadlines, and hands changed dependencies to the prepared-stale trace; +- uniform authority executes the protected-route inventory and observes policy identity at each ingress; +- tombstone replay creates selector state in the database, presents fresh evidence through a real ingress, and verifies no mutation; +- final-denial tests inspect every authoritative store, prove zero denied receipts, and exercise available and exhausted denial-observation channels; and +- trusted-proxy spoof tests run against the deployed listener boundary. + +The following are not behavioral adapters: + +- a regular expression over source or generated schemas; +- a test that checks only that a function or route exists; +- a documentation-link check; +- a mocked proxy-isolation test; or +- a passing result copied from another revision. + +## Pull request evidence + +A runtime or deployment change includes: + +- exact parent and head revisions; +- owned behavior and affected stable labels; +- implementation and adapter commands; +- services, fixtures, and fault injection used; +- evidence artifact digests; +- migrations and rollback behavior when state changes; +- route and discovery impact; +- privacy and observability impact; and +- explicit unsupported behavior. + +Do not describe a feature as conforming because it compiles, parses configuration, or has a green source-string test. State whether the full exact-head matrix ran and link its immutable report. + +## Review checklist + +### Protocol and model + +- Definitions, pseudocode, state, and traces agree. +- Stable policy identity excludes rotating key material. +- Assertion expiry bounds authority but not durable binding lifetime. +- Lifecycle selectors are checked before ordinary enrollment. +- Preparation remains read-only. +- Final admission rereads only applicable direct or delegated witnesses. +- Public denials remain many-to-one and privacy safe. + +### Runtime + +- Every protected ingress uses one current authority and policy lineage. +- The selected transport comes only from server configuration and never falls back. +- Every profile produces the same closed normalized result and reaches the same final-admission authority. +- When HMAC-v2 is configured, provenance covers the exact canonical request and retains replay state long enough. +- A registered authenticated-edge adapter proves immediate-caller authentication, origin isolation, request integrity, field stripping, upstream-policy validation, and its mechanism-specific replay behavior. +- Binding and lifecycle decisions are serialized under concurrency. +- When a local JWT/JWKS verifier is configured, JWKS addition, removal, hard expiry, and outage behavior are exercised; other adapters exercise their authenticated current-policy dependencies. +- Lease reuse checks current dependencies. +- Application denial leaves no authority or application mutation, creates no authorization receipt, and remains effective when denial observation is unavailable. + +### Lifecycle + +- Provisioning requires target-key proof. +- Retirement, disablement, and revocation preserve lineage. +- Rotation starts from one exact active binding and leaves no pending replacement. +- Recovery consumes one exact pending lineage for an enabled identity. +- Re-enablement handles disabled identities and consumes present lineage once. +- Administrative expiry does not create a tombstone or free coordinates. +- Rollback uses a compensating privileged transition instead of database rewind. + +### Deployment and operations + +- Direct origin access to trusted-proxy ingress is blocked and tested. +- Secret references contain no secret values in repository files. +- Startup denies uncovered routes or competing authorities. +- Backup and restore include lifecycle selectors, policy generations, receipts, and audit state. +- Required-dependency outages and authorization-audit capacity limits fail closed and alert; separately bounded denial-observation exhaustion drops observation without weakening the denial and emits saturation signals. +- A deployment claiming durable JWKS rollback prevention supplies an authenticated monotonic version or key floor; the base current-snapshot contract makes no such claim. + +### Evidence + +- All 24 traces appear exactly once in the report. +- Every required trace passes at the claim tuple. +- Proxy and verifier-parity evidence matches the claim's transport-contract revision and profile-contract digest. +- Every `not-applicable` trace has executable absence evidence. +- Denial-oracle evidence uses a fixed iteration count, predeclared bounds and + statistical rule, a pinned isolated runner, and no automatic retry after a + threshold breach. +- Artifacts and digests resolve. +- Privacy canaries are absent from every public and operational sink. + +## Documentation checks + +Documentation-only changes run, at minimum: + +```sh +git diff --check +jq empty docs/examples/nip-fi-*.json.example +``` + +They also validate local Markdown links, code fences, table structure, example-to-normative label equality, and matrix-to-normative label equality. These checks validate documentation consistency. They do not prove runtime conformance. + +Runtime and deployment changes additionally run the repository's normal gates and every applicable exact-head behavioral adapter. + +## Security-sensitive changes + +Treat verifier rules, normalized-result mappings, profile registration, proxy canonicalization, secret rotation, replay retention, lifecycle transitions, privacy filters, denial mapping, delegation, lease invalidation, restore, and rollback as security-sensitive. Update the [threat model](NIP_FI_THREAT_MODEL.md) when a trust boundary, asset, attacker capability, or residual risk changes. + +Never place real assertions, subjects, issuer-private values, HMAC secrets, registered profile identifiers, private deployment fields, operator credentials, or production evidence in examples or test fixtures. + +## Commit and handoff + +Use focused conventional commits with DCO sign-off. Preserve human-authored history and public review text. Before handoff, report exact commit, tree, and parent identities; changed paths; checks; stable labels affected; and remaining implementation or deployment dependencies. diff --git a/docs/NIP_FI_DEPLOYMENT.md b/docs/NIP_FI_DEPLOYMENT.md new file mode 100644 index 00000000000..d817cb4032b --- /dev/null +++ b/docs/NIP_FI_DEPLOYMENT.md @@ -0,0 +1,160 @@ +# NIP-FI stock deployment + +This guide describes a provider-neutral stock deployment for the normative [NIP-FI contract](nips/NIP-FI.md). It does not by itself activate support. + +## Availability boundary + +This documentation revision does not add a NIP-FI runtime parser, adapter, discovery, or enforcement. The proposed `BUZZ_NIP_FI_V1_CONFIG_JSON` document, operating modes, field bounds, and future startup rejection rules are recorded in the [identity configuration contract](CORPORATE_IDENTITY.md); they are not claims about the current relay binary. + +After a later implementation supplies that parser, configuring Enforce is still not conformance. A deployment must not advertise or enforce NIP-FI until it passes the [behavioral evidence matrix](nips/NIP-FI-CONFORMANCE.md) at the deployed revision. + +The files under [`docs/examples`](examples/) are review templates. They are not accepted runtime schemas and contain no deployable secrets. + +## Stock topology + +```text +external client + -> TLS listener or trusted edge + -> Buzz verifier ingress + -> one canonical NIP-FI normalized-result and final-admission authority + |-> binding/lifecycle, replay, policy, receipt, authorization-audit, and application stores + |-> separately bounded, non-authoritative denial observations + `-> configured assertion-policy source over authenticated transport +``` + +The stock profiles are provider free. They identify issuers by exact configured values and retrieve keys under bounded policy. They do not require a provider-specific sidecar, claim dialect, SDK, or forwarded-identity convention. + +## Activation prerequisites + +Do not publish NIP-FI discovery or enable enforcement until all of these are true for one immutable claim tuple: + +1. The running artifact supplies a canonical normalized-result contract, final-admission authority, lifecycle state, and executable adapter manifest. +2. Every protected WebSocket and HTTP ingress appears in the executed route inventory. +3. One current domain policy and policy lineage covers every protected ingress. +4. Assertion-policy source, identity/key/claim mapping, time, size, authenticated snapshot, and enrollment policy is explicit for each domain; JWT policies also define issuer, audience, algorithm, and JWKS behavior. +5. Binding, lifecycle, replay, receipt, authorization-audit, invalidation, and application storage meets serialization and durability requirements; denial observations use a separate finite-capacity non-authoritative channel. +6. Secrets arrive from an access-controlled secret store and never from repository examples. +7. Backup, restore, key rotation, dependency outage, recovery, and rollback exercises pass. +8. All applicable `FI-TRACE-*` rows pass at the exact implementation, adapter, artifact, deployment, and policy digests. + +A source scan, config parse, rendered manifest, healthy process, or documentation link closes none of these gates. + +## Domain policy + +Use one reviewed policy object per server-selected domain. The review-only [stock domain example](examples/nip-fi-stock-domain.json.example) records the required decisions without claiming a runtime schema. + +The runtime document and its fixed verifier semantics together represent: + +- exact domain identity and trusted listener or route mapping; +- accepted issuer and audience values; +- allowed compact-JWS algorithms and compatible key types; +- subject and optional Nostr-key claim rules, plus the operator evidence that the issuer subject is stable and non-reassignable; +- assertion, clock-skew, header, and body bounds; +- authenticated policy-snapshot refresh, stale-on-error, and hard-validity bounds, including JWKS behavior for JWT policies; +- exactly one enrollment mode; +- exactly one server-selected assertion transport per bound route and domain, with its transport-contract revision and profile-contract digest; +- delegation support and positive finite maximum, when enabled; +- stable public denial mapping; +- bounded private denial observation with stable reason and correlation identifiers, minimal attacker-controlled payload, and no authorization effect; and +- current policy identity and lineage, including the authenticated key or upstream-policy source identity and compiled verifier-contract fingerprint. + +Rotating authenticated key or upstream-policy snapshot contents changes the applicable generation, not the stable verifier-policy identity. Changing accepted assertion semantics creates a new policy identity. For JWT policies, verification-key rotation changes the JWKS generation. + +The stock contract compares against the current authenticated JWKS snapshot and does not promise durable anti-rollback. If an old set is republished, its keys may become current again. A deployment that needs rollback prevention adds an authenticated monotonic version or durable key floor and corresponding evidence. + +## Client-attached profile + +Expose `client-attached` only where the client can send exactly one `Nostr-Federated-Identity: Bearer ` field and fresh Nostr proof through one protected request flow. Use that assertion field for both WebSocket upgrades and HTTP requests. Never accept the federated assertion in `Authorization`. Reject assertion-provenance fields on this profile and never fall back to it after trusted-proxy evidence is missing, mixed, or invalid. + +Treat assertions as confidential bearer material. Do not log headers, echo them in errors, retain them in public history, or forward them beyond the canonical verifier. + +## Trusted-proxy profile + +The Buzz stock trusted-proxy profile is `trusted-proxy-hmac-v2`; v1 is rejected. A deployment is not required to implement or advertise it. When selected, every applicable request requires valid HMAC-v2 evidence and cannot fall back to another profile. The edge: + +1. removes every inbound assertion, provenance, and client-peer field, including attacker-supplied duplicates; +2. completes trusted routing and canonical path rewriting; +3. computes the assertion digest and request body digest; +4. creates a fresh timestamp and nonce; +5. canonicalizes the authenticated end-client IP and selects the authorization domain and proof-transport code; +6. MACs the exact timestamp, nonce, assertion digest, authorization domain, method, authority, path/query, body digest, proof transport, and client peer; and +7. sends exactly one `Nostr-Federated-Identity: Bearer ` field, one `Nostr-Federated-Identity-Provenance` field, and one `Nostr-Federated-Identity-Client-Peer` field to verifier ingress. + +The HMAC secret is random, access controlled, versioned, and delivered independently of application configuration. Rotation uses a short, explicit overlap. Replay uniqueness is scoped to the trusted-proxy domain, profile, and nonce and is independent of which active secret verified the MAC. A nonce re-signed with another active secret remains a replay. The matched secret version may appear only in private audit metadata. Remove the old version after all requests and replay windows expire. + +Network controls ensure that only the trusted edge can reach verifier ingress. A separate health or administrative listener cannot proxy protected operations. The deployment test runs direct-origin, bypass, client-header injection, mixed-profile, replay, and cross-request mutations from both sides of the boundary. A mocked listener test cannot satisfy `FI-TRACE-PROXY-SPOOF`. + +## Registered trusted-edge profile + +A deployment may install one private registered trusted-edge profile whose identifier matches `x---v`. The identifier and its mechanism, fields, caller identity, issuer, and topology stay out of NIP-11 and public examples. Clients cannot request or infer it. + +The reviewed profile contract identifies either request-bound evidence or an authenticated-edge assertion adapter. It closes every authoritative field, provenance rule, positive finite provenance bound, protected request component, replay semantic, and assertion-policy adapter identity. Equality is expired. The selected assertion policy separately closes normalized-result semantics. An authenticated-edge adapter proves cryptographic immediate-caller authentication, accepting-origin isolation, full integrity for authorization-relevant request components, inbound-field stripping, and validated upstream policy projection together. + +The deployment record identifies the trusted edge, accepting origin, direct-origin controls, field-stripping point, caller authentication, protected components, upstream assertion validation, Nostr-proof path, compromise impact, and evidence location. It binds all proxy and verifier-parity evidence to the exact transport-contract revision and profile-contract digest. + +If a JWT-based profile permits bearer reuse, an unexpired JWT may be presented again only with a fresh request-appropriate Nostr proof and current binding, lifecycle, policy, and final-admission state. The deployment does not claim single-use JWT semantics unless the profile defines and proves them. + +## Storage and transactions + +Production-equivalent storage must provide: + +- serialized uniqueness for both sides of each domain's active binding relation; +- durable lifecycle selectors and immutable typed history; +- monotonic binding and lifecycle versions; +- atomic enrollment, replay claim, request-bound receipt, and required authorization audit evidence; +- idempotent application consumption when the application effect uses another transaction; +- bounded replay retention that outlives the accepted provenance window; +- a separately capacity-bounded denial-observation channel whose failure never changes or delays denial and never creates an authorization receipt; +- current policy, authenticated snapshot generation, resource, and relationship witnesses; and +- backup and restore consistency across authority state. + +Caching may improve reads but cannot authorize from state older than its witnessed invalidation bound. An unavailable cache origin, database, replay store, receipt store, or required authorization-audit sink denies. An unavailable or exhausted denial-observation channel drops or truncates observation while the denial stands and authoritative stores remain unchanged. + +## Rollout + +### 1. Inventory + +List every protected ingress and its current authorization path. Include WebSocket operations, HTTP event/query/count, media, Git, audio, invite, moderation, and operator paths. Remove or fail startup on uncovered and competing authorities. + +### 2. Install without discovery + +After the implementation stack supplies the proposed configuration contract, deploy its exact artifact with NIP-FI discovery and enforcement off using the implementation's reviewed fail-closed mechanism. Load reviewed policy and secret references. Validate assertion-policy source connectivity and authenticity, including JWKS for JWT policies, plus state migrations, backup, observability redaction, and route inventory without creating production bindings. + +### 3. Run isolated behavior + +Run every applicable adapter in an isolated namespace using synthetic issuers, identities, keys, and domains. Retain commands, results, state snapshots, wire captures, sink scans, and artifact digests. + +### 4. Exercise the deployed boundary + +Run production-equivalent trusted-edge negative tests, dependency fault injection, restore, key rotation, concurrency, lifecycle, recovery, and rollback exercises. Do not infer these outcomes from isolated unit tests. + +### 5. Activate atomically + +Only after the exact claim tuple passes, publish discovery and enable one canonical authority for the complete protected-ingress set. Do not canary by leaving some protected routes under an older identity authority. Canary domains or isolated deployments instead. + +### 6. Observe without identity leakage + +Monitor aggregate allow/deny classes, stable private denial reasons, denial-observation drops and saturation, dependency readiness, refresh age, replay pressure, final-admission conflicts, lease invalidations, and authorization-audit capacity. Private reason details stay access controlled. Raw assertions and identity claims are never metric labels or trace attributes, and denial records never contain raw tokens or verbatim unverified claims. + +## Rollback + +Rollback means returning to a previously conformant artifact and compatible policy/state lineage, or disabling NIP-FI discovery and failing the protected operation closed. It does not mean accepting unsigned identity, restoring a removed verification key, deleting tombstones, rewinding lifecycle tables, bypassing audit, or running a legacy authority beside NIP-FI. + +Before rollout, record compatible artifact, policy, migration, and storage checkpoints. If a schema or semantic change is not backward compatible, use a reviewed forward repair or compensating privileged transition. Follow the [runtime operations guide](NIP_FI_RUNTIME_OPERATIONS.md) for authority-state recovery. + +## Compose and Helm + +The current [Compose](../deploy/compose/README.md) and [Helm](../deploy/charts/buzz/README.md) bundles contain no NIP-FI adapter wiring. Their NIP-FI sections are readiness statements, not activation instructions. + +A later bundle must: + +- pin the exact implementation artifact and expose its adapter/config version; +- mount domain policy and secret references without putting secrets in values or examples; +- isolate trusted edge and verifier ingress when the proxy profile is enabled; +- provide readiness that covers every fail-closed dependency; +- name the backup, restore, migration, and rollback procedures; and +- link the matching immutable conformance report. + +## Deployment record + +For each enforcing environment, retain the immutable claim tuple, policy and artifact digests, route inventory, accepted profiles, transport-contract revision, immutable profile-contract artifacts and digests, enrollment mode, delegation posture, secret versions without secret values, storage topology, executed adapter report, restore exercise, activation time, and rollback target. diff --git a/docs/NIP_FI_INTEGRATION.md b/docs/NIP_FI_INTEGRATION.md new file mode 100644 index 00000000000..dd1c5dcbd00 --- /dev/null +++ b/docs/NIP_FI_INTEGRATION.md @@ -0,0 +1,251 @@ +# NIP-FI integration contract + +This guide describes how a Buzz implementation integrates the normative [NIP-FI specification](nips/NIP-FI.md), [formal model](nips/NIP-FI-MODEL.md), and [behavioral evidence matrix](nips/NIP-FI-CONFORMANCE.md). It is non-normative and does not weaken those documents. + +## Current status + +This documentation revision does not add a runtime adapter, activate enforcement, or establish conformance. A later implementation stack must supply exact-head behavioral adapters for all applicable `FI-TRACE-*` identifiers. NIP-FI discovery and enforcement remain off until one immutable implementation and deployment tuple passes the release gate. + +Source review, type presence, configuration parsing, route registration, and prose checks are useful review inputs. None proves runtime behavior. + +### Delegation availability + +NIP-FI delegation is an optional protocol capability, not an implied Buzz +feature. The current public production path has no reviewed delegated-owner +implementation and MUST NOT advertise delegation. Discovery reports +`"delegation": false` or omits the field, and operators keep delegation +disabled. A downstream or legacy configuration switch cannot create the +missing issuance, owner-resolution, expiry, invalidation, reconnect, and +protected-transport behavior. + +This documentation-only revision does not change runtime code or configuration +defaults. Operators must verify the effective setting in their deployment; +public wording alone is not evidence that the runtime or its defaults satisfy +this boundary. + +The specification, model, sealed types, and conformance vectors may describe +future delegation work without claiming that a route is live. Support can be +advertised only after one reviewed implementation supplies the separate +delegated-owner capability and passes every applicable delegation and session +trace across every advertised transport. Until then, any partially wired +delegation-shaped request must fail closed. + +## Integration boundary + +NIP-FI is an additional admission authority above NIP-42 or NIP-98 proof of key control. It does not replace Nostr signatures or application authorization. A protected operation proceeds only when all applicable gates agree: + +```text +trusted route and domain + + accepted assertion transport or delegated evidence + + fresh Nostr proof + + current binding and lifecycle state + + current verifier, policy, and resource state + + final application admission + = committed authorization +``` + +A configured deployment uses one canonical current NIP-FI authority for every protected ingress in a domain. It cannot run a legacy identity authority beside NIP-FI, select an older policy lineage for one route, or leave a protected route Nostr-only. + +## Protected-ingress inventory + +The implementation stack owns an executable inventory of every protected operation. At minimum, reviewers classify and exercise: + +- WebSocket connection authentication and each protected message or event path; +- HTTP event submission, query, and count operations; +- media reads, writes, and metadata operations; +- Git smart HTTP and policy hooks; +- audio and real-time session operations; +- invite and moderation operations; and +- lifecycle or operator operations. + +The inventory records the server-selected domain, operation, resource, transport profile, policy identity, and final-admission adapter for each route. Unknown protected routes and incompatible policy lineages fail closed. `FI-TRACE-AUTHORITY-UNIFORM` executes the inventory rather than inspecting route source. + +## Server-owned context + +Trusted listener and route configuration resolves the target context before identity evidence can influence a decision. The Nostr proof then seals the actor key into the request context. + +The implementation must not accept a client domain, forwarded authority, assertion claim, tag, or query parameter as a domain selector. If a trusted edge rewrites authority or path, it completes the rewrite before producing configured profile evidence. HMAC-v2 computes its MAC only after that rewrite, and the relay reconstructs the same canonical post-routing values. + +Multi-domain deployments use the same authenticated domain lookup for authorization and discovery. An unconfigured domain receives no inherited NIP-FI profile. + +## Canonical assertion verifier + +Every transport adapter produces one closed normalized verified assertion result and reaches the same final-admission authority. The stock adapters call one provider-neutral compact-JWS verifier. A registered adapter either calls that verifier or authenticates its closed upstream assertion and authorization claim set before producing the same result. Ordinary forwarded headers and adapter-local authorization cannot enter it. Operators separately establish that each configured issuer's subject is stable and non-reassignable; one request cannot prove that property. + +The stable verifier policy identity deterministically covers every configured assertion-semantic input—including identity/key/claim mapping, bounds, normalization, authenticated key or upstream-policy source identity, and applicable algorithm rules—plus a versioned fingerprint of compiled acceptance rules. It excludes transport and rotating key or upstream-policy snapshot contents. Prepared and direct-lease evidence retains the current profile and assertion-policy dependencies plus confidential material needed to revalidate the authoritative assertion input. + +For a JWT policy, a separate JWKS generation identifies the effective key snapshot. On a generation change, final admission and lease reuse revalidate the original assertion against the current snapshot. A retained key can continue to authorize. A key absent from the current snapshot, an unreadable current snapshot, or a hard-expired snapshot denies. The base JWT contract has no durable anti-rollback oracle, so republication of an old authenticated set can make its keys current again. A non-JWT registered adapter defines equivalent authenticated current-policy snapshot dependencies and revalidation. `FI-TRACE-VERIFIER-PARITY` proves the common result; applicable JWT deployments also run `FI-TRACE-JWKS-ADD` and `FI-TRACE-JWKS-REMOVE`. + +## Assertion transport + +The selected profile, transport-contract revision, and profile-contract digest are part of trusted target context. Server-owned listener, route, and authorization-domain configuration selects exactly one profile before accepting protected traffic. The implementation supports only complete profiles and never falls back after mixed, missing, or rejected evidence. Clients cannot select, negotiate, or downgrade the profile. + +### Client attached + +The `client-attached` adapter accepts exactly one `Nostr-Federated-Identity: Bearer ` field and rejects assertion-provenance fields. The same assertion field is used for WebSocket upgrades and HTTP requests. `Authorization` remains reserved for Nostr proof where required and never carries the federated assertion. HTTP evidence and NIP-98 proof arrive on the same request. WebSocket assertion evidence arrives on the authenticated upgrade and is retained confidentially only for the resulting admission or lease. + +### Trusted proxy + +The `trusted-proxy-hmac-v2` adapter implements the exact envelope, canonical request bytes, time bounds, nonce size, HMAC, client-peer field, and replay retention in NIP-FI. The trusted edge removes inbound assertion, provenance, and client-peer fields before setting its own. v1 envelopes are rejected. + +HMAC-v2 is the portable stock trusted-edge profile, not a mandatory deployment choice. When selected, every applicable request requires valid HMAC-v2 evidence and failure never falls back to `client-attached` or another adapter. + +The deployment also proves: + +- untrusted clients cannot reach verifier ingress; +- direct origin requests deny; +- mixed profiles deny without fallback; +- client header injection denies; +- nonce replay commits at most once; and +- changing the domain, proof transport, authenticated client peer, or any other request-bound field denies. + +Local unit tests cannot prove network isolation. The deployment bundle must retain live negative evidence for `FI-TRACE-PROXY-SPOOF`. + +### Registered trusted edge + +A deployment may install one private registered profile matching `x---v`. Its contract identifies either request-bound evidence or an authenticated-edge assertion adapter and closes every authoritative field, provenance rule, deadline, protected request component, replay semantic, and assertion-policy adapter identity. The selected assertion policy separately closes the normalized-result semantics. Registered identifiers and private mechanism details remain outside NIP-11 and public examples. + +An authenticated-edge adapter must prove cryptographic immediate-caller authentication, accepting-origin isolation, full integrity for authorization-relevant request components, inbound-field stripping, and validated upstream policy projection together. It records mechanism-specific spoof, replay, cross-request, and verifier-parity evidence at the exact implementation, adapter, deployment, policy, transport-contract revision, and profile-contract digest. + +If a JWT-based adapter permits bearer reuse, reusing an unexpired JWT with a fresh request-appropriate Nostr proof is not itself a proxy-replay failure. Expired assertions, replayed or transplanted Nostr proofs, presentation outside the configured edge, and every mechanism-specific captured-artifact violation still deny. + +## Binding and lifecycle state + +Storage represents the formal model's effective state: + +- `B`: active durable bindings and immutable provenance; +- `T`: retired exact identity/key pairs; +- `X`: disabled identities; +- `Y`: domain-scoped revoked keys; +- `Q`: pending-replacement lineage; +- `H`: immutable typed lifecycle history; and +- `V`: monotonic binding and lifecycle versions. + +An implementation may use different names or tables, but the behavioral selectors remain distinct. A history timestamp is not an active selector. Assertion expiry never becomes binding expiry. Optional administrative expiry is separately authorized and versioned. + +Hot-path authorization reads both sides of the active relation and every applicable selector. It does not rely only on writer invariants. Unreadable or contradictory state denies. + +## Prepared and committed authorization + +Preparation is read-only. It creates no authoritative state, publication, or last-seen value. A denied preparation may attempt the separately bounded, non-authoritative denial observation only after the decision is fixed. + +Prepared evidence uses one of two dependency sets: + +- `DirectPrepared`: assertion, optional proxy provenance, actor binding and lifecycle witnesses, and enrollment-mode witness; or +- `DelegatedPrepared`: delegation, current owner binding and lifecycle witnesses, and relationship witness. + +Both include exact request context, fresh Nostr proof, local policy and resource witnesses, deadlines, and invalidation dependencies. + +Final admission rereads only applicable witnesses inside the authorization transaction. Unreadable state denies. Changed state requires a complete recomputation and may commit only a semantically equivalent current result. Identical concurrent enrollment may converge on the same binding version; a conflicting result denies. + +Replay claims, eligible enrollment, request-bound receipt, and required authorization audit evidence commit together. If the application effect uses another transaction, it consumes a request-bound idempotent receipt so retry cannot duplicate the effect. + +A denied operation creates no authorization receipt. It attempts a stable reason code and correlation identifier through the non-authoritative denial channel. That channel has a finite capacity separate from required authorization audit evidence; an unavailable or exhausted channel cannot weaken, delay, retry, or reverse the denial. + +## Enrollment modes + +- `attested-key` creates a first binding only when the assertion's key claim equals the proven key. +- `provisioned` never creates a binding from ordinary authorization. A privileged transition still requires fresh target-key proof and any configured issuer attestation. +- `tofu` is risk-labelled. It may bind an attacker's key when the attacker has a stolen assertion for a never-enrolled identity. + +Mode changes affect future creation only. They do not rewrite existing bindings or downgrade provenance. A matching key claim in TOFU records `attested-key`, not `tofu`. + +## Lifecycle operations + +Provision, retire, disable, revoke, rotate, recover, re-enable, and administrative-expiry changes are separately authorized transitions. Each binds authority to the domain, operation, request, identity, old version when present, and target key when present. + +Every new target key proves control. Replacement provenance reflects the evidence used for that key. A privileged transition without matching issuer key attestation records provisioned provenance; it never inherits TOFU or attested provenance from another key. + +Rotation starts from an active binding and leaves no pending lineage. Recovery consumes exact pending lineage for an enabled identity. A disabled identity uses re-enablement, which requires an eligible target key with fresh proof and creates the binding while clearing disabled state. There is no clear-only transition: without a target, provisioned mode has nothing to match and TOFU could expose a first-use resurrection window. Retired pairs and revoked keys remain durable. + +See [runtime operations](NIP_FI_RUNTIME_OPERATIONS.md) for preconditions, postconditions, recovery, and rollback rules. + +## Sessions and delegation + +HTTP authorization applies to one request. A WebSocket lease is per key, domain, capability, resource, normalized result, and exact dependency set. Its deadline is the earliest normalized authority deadline and every applicable transport, proof, binding, delegation, policy, and implementation bound. JWT profiles include assertion and key-snapshot deadlines; every profile supplies at least one finite authority deadline. Equality is expired. + +This section defines the contract for future delegation support; the current +[delegation availability](#delegation-availability) statement remains +controlling for Buzz deployments. + +Direct lease reuse rechecks current binding and lifecycle versions plus every profile and assertion-policy dependency; changed dependencies require equivalent normalized-result revalidation. JWT dependencies include the key-snapshot hard deadline and JWKS generation. Delegated lease reuse rechecks the current exact owner binding and relationship revision. A lease for one key never covers another key on the connection. + +Delegation is optional and separate from federated assertion transport. The delegate supplies its own fresh Nostr proof and no assertion or assertion-provenance field. The owner must remain current and eligible at preparation and final admission. Rotation does not transfer delegation to the new owner key. + +## Discovery and privacy + +Discovery is a claim, not a feature flag. The service advertises only implemented stock profiles and optional behavior that passed the evidence matrix at the running implementation and deployment revision. Registered profile identifiers and private mechanism details never appear in NIP-11. An implementation without a complete stock adapter omits NIP-FI discovery. + +NIP-FI defines no public identity projection. Assertions, issuer-qualified identities, profile claims, HMAC correlation values, and private policy state stay out of protocol output and public history. Access-controlled enforcement state retains only what lifecycle, audit, and incident response need. + +Public denials use the stable classes from NIP-FI. They do not reveal whether an identity, key, binding, tombstone, claim, enrollment mode, or private policy exists. + +Private denial observations contain no raw tokens or verbatim unverified claims and use only bounded or keyed-hashed source coordinates. Their absence does not prove that no denial occurred, and authorization, lockout, or rate-limit policy does not depend on them. + +## Implementation-stack handoff + +The later implementation stack must deliver, at one exact head: + +1. Recheck binding version, lifecycle version, invalidation state, every + authenticated policy-snapshot hard deadline, and current snapshot + generation before every protected WebSocket use. JWT evidence revalidates + the retained assertion when JWKS generation changes. +2. Install exactly one server-selected transport for each bound route and + domain, with no request negotiation or fallback. +3. Preserve the HMAC-v2 envelope, canonicalization, time bounds, nonce ledger, + replay behavior, secret rotation, and fixtures unchanged when that stock + profile is selected. +4. Install a registered adapter only after its closed profile contract, + normalized-result mapping, deployment evidence, and profile-contract digest + are reviewed. Permission in these documents does not mean the current + runtime implements such an adapter. +5. Bound every lease by all assertion, provenance, authenticated policy + snapshot, proof, binding, local-policy, and implementation deadlines. +6. Register every issued lease with the invalidation registry, meaning the + implementation's generic lease-cancellation owner, or an equivalent owner + that supplies immediate version fencing and bounded post-commit closure. +7. Preserve evidence for the exact request, actor, proof transport, admission, + adapter, deployment, policy, transport-contract revision, and + profile-contract digest. +8. Demonstrate the selected profile's spoof, replay, cross-request, + verifier-parity, and final-admission properties before claiming + conformance. + +The same handoff also includes the complete supporting deliverables: + +- a route-adapter manifest for every protected ingress; +- one canonical normalized-result contract and shared authorization corpus; +- serialized lifecycle storage with selector-conflict fixtures; +- read-only preparation and atomic final admission; +- direct and delegated lease dependency revalidation; +- an executable adapter mapping for every applicable `FI-TRACE-*` label; and +- a conformance report whose revisions, profile contracts, and artifact + digests match the exact deployment. + +The configuration migration is explicit: add +`configuration_contract_revision`; replace the flat issuer shape with +`domains[]` and nested `domains[].issuers[]`; add `denial_observation`; remove +the earlier client-presentation-capacity object and restore map; and replace +the earlier nonstandard enrollment value with the `attested-key`, +`provisioned`, and `tofu` enum. Parser behavior, validation, fixtures, policy +digests, and adapter evidence move to that shape together. The implementation +must reject the obsolete shape rather than treat it as an alias. + +These are implementation obligations, not claims about present runtime +behavior. The implementation stack may remain HMAC-v2-only until a deployment +chooses and implements a registered profile. This documentation change does +not require deleting the HMAC envelope, secrets, nonce ledger, +canonicalization, replay machinery, or fixtures. The new configuration and +trace contracts do require a revised implementation plan and evidence. + +The implementation handoff also lists exact commands, required services, +fault-injection controls, and cleanup steps. A source grep, documentation link, +or claim that code paths are wired is not a substitute. + +## Related guidance + +- [Threat model](NIP_FI_THREAT_MODEL.md) +- [Stock deployment](NIP_FI_DEPLOYMENT.md) +- [Runtime operations](NIP_FI_RUNTIME_OPERATIONS.md) +- [Contributor guide](NIP_FI_CONTRIBUTING.md) +- [Legacy corporate identity migration](CORPORATE_IDENTITY.md) diff --git a/docs/NIP_FI_RUNTIME_OPERATIONS.md b/docs/NIP_FI_RUNTIME_OPERATIONS.md new file mode 100644 index 00000000000..b61665df443 --- /dev/null +++ b/docs/NIP_FI_RUNTIME_OPERATIONS.md @@ -0,0 +1,160 @@ +# NIP-FI runtime operations + +This guide covers lifecycle, recovery, restore, and rollback for a conforming Buzz NIP-FI implementation. The normative [specification](nips/NIP-FI.md) and [formal model](nips/NIP-FI-MODEL.md) control when this guide differs. + +## Availability boundary + +This documentation revision supplies no NIP-FI runtime parser or operator tooling. The proposed future `BUZZ_NIP_FI_V1_CONFIG_JSON` contract and operating modes are documented in the [identity configuration contract](CORPORATE_IDENTITY.md), but are not claims about the current relay binary. Once an implementation exists, its own reviewed tooling defines lifecycle command names and request schemas; the [recovery-plan example](examples/nip-fi-recovery-plan.json.example) remains a review template, not a runtime request. + +Do not simulate an operator transition with direct database edits. Every authority change uses a separately authorized, audited transition with fresh target-key proof when a new key is introduced. + +## Operating principles + +- Keep ordinary authorization separate from privileged lifecycle authority. +- Resolve the domain, operation, identity, current version, and target key from one sealed operator request. +- Check both sides of the active binding and every lifecycle selector under serialization. +- Preserve retired pairs, revoked keys, disabled identities, pending lineage, typed history, and immutable provenance. +- Treat preparation and denial as read-only with respect to authoritative state; emit denial observations only through the separately bounded non-authoritative channel. +- Commit final authority state, receipt, replay claim, and required audit evidence atomically. +- Atomically commit current authority versions and a durable invalidation intent. Fence new and reused authority on those versions immediately, then close affected cached leases after commit within the documented detection bound. +- Use compensating transitions for repair; never rewind authority tables. +- Fail closed when current policy, state, receipt, replay, audit, or invalidation dependencies are unreadable. +- Never create an authorization receipt for a denied operation or make denial depend on an observation write. + +## Routine readiness + +At startup and continuously, verify: + +- one current policy lineage covers every protected ingress; +- the canonical verifier can read an authentic current key snapshot within its hard bound; +- binding and lifecycle stores are readable and constraints are healthy; +- replay, receipt, audit, relationship, policy, resource, and invalidation stores are available; +- clock synchronization stays inside configured bounds; +- trusted-proxy ingress is isolated and current HMAC versions are known; +- queues, replay retention, audit capacity, and JWKS refresh age remain inside limits; and +- the deployed artifact, adapter, policy, and route inventory still match the recorded conformance tuple. + +Readiness for protected operations fails when a required dependency fails. Liveness may remain available for diagnosis but cannot become a bypass route. + +## Lifecycle transition table + +| Transition | Required current state and authority | Committed result | Required evidence | +|---|---|---|---| +| Provision | Provisioned enrollment mode; no conflicting active pair or pending lineage; eligible identity and key; privileged authority; fresh target-key proof; configured issuer attestation when required | Fresh active binding with provisioned provenance and new version | Sealed request, target proof, policy and state witnesses, receipt, typed history, and lease invalidation | +| Retire pair | Exact active identity/key pair and expected version | Binding removed, exact pair added to retired state, exact pending lineage recorded | Old and new versions, selector snapshots, receipt, history, and affected lease closure | +| Disable identity | Current enabled identity; exact active version when bound | Identity disabled; active pair retired and pending lineage recorded when present | Identity selector, optional pair/version, receipt, history, and direct/delegated invalidation | +| Revoke key | Domain-scoped key, active or inactive | Key remains revoked; active pair is removed, retired, and recorded as pending when present | Key selector, idempotent-repeat result, optional pair/version, receipt, history, and invalidation | +| Rotate | Exact active old binding/version, eligible new key, no pending lineage, fresh new-key proof | Old pair retired; new binding created at a fresh version; no pending lineage remains | Privileged transition authority, exact old binding/version, fresh new-key proof, selectors, provenance decision, receipt, history, and owner/delegate lease effects | +| Recover | Enabled identity with one exact pending lineage and no active binding; eligible new key; fresh new-key proof | Pending lineage consumed once; new binding created; old retired pair preserved | Pending version, target proof, selector snapshots, receipt, history, and invalidation | +| Re-enable identity | Disabled identity and either no prior lineage or one exact pending lineage; eligible key; fresh key proof | Disabled state cleared; present lineage consumed once; new binding created | Disabled and lineage selectors, target proof, receipt, history, and invalidation | +| Set or clear administrative expiry | Exact active binding/version and privileged authority | Administrative bound and version updated without retiring the pair or freeing either coordinate | Old/new bound, controlled-clock checks, receipt, history, and lease deadline change | + +Repeated authorized revocation may be idempotent but cannot erase lineage. A retry of any transition uses its request-bound receipt and must not create a second history fact or binding version. + +## Recovery decision + +Use the lifecycle state, not the operator's desired outcome, to choose the transition: + +- An enabled identity with exact pending replacement lineage uses **Recover**. +- A disabled identity uses **Re-enable identity**, whether or not pending lineage exists. +- An active binding moving directly to a new key uses **Rotate**. +- An identity with no active binding and no lineage uses **Provision** when policy allows it. +- A binding blocked only by administrative expiry uses the separately authorized administrative-expiry transition. + +Re-enable identity always requires an eligible target key and fresh proof and +creates the new binding in the same transition that clears disabled state. A +clear-only operation would create a resurrection window: provisioned mode +would have no target to match, while TOFU could let the next ordinary +admission capture first use. To re-enable now and provision later, leave the +identity disabled until the target and proof are available. + +If selectors are contradictory, versions are unknown, or history and active state disagree, stop. Preserve the evidence, fail closed, and investigate before authorizing a forward repair. + +## Recovery procedure + +1. Open an incident or change record and identify the server-selected domain. +2. Read the active binding from both identity and key directions plus `T`, `X`, `Y`, `Q`, `H`, and `V`. +3. Select Recover, Re-enable, Rotate, Provision, or administrative-expiry change using the decision above. +4. Obtain separately authorized operator approval and fresh proof from every new target key. +5. Prepare the transition without mutation and record its exact versions and policy witnesses. +6. At final admission, reread the applicable selectors and atomically commit the transition, receipt, history, audit evidence, current authority versions, and durable invalidation intent. +7. Fence authorization on the committed versions immediately and close affected cached leases after commit within the documented detection bound. +8. Verify the intended postconditions and that old pairs, revoked keys, and disabled or pending selectors changed only as defined. +9. Exercise old-key, old-delegation, replay, cross-domain, and new-key cases through production-equivalent ingress. +10. Retain privacy-safe evidence and close the change only after state and lease checks pass. + +The review-only [recovery-plan example](examples/nip-fi-recovery-plan.json.example) helps record this plan without containing real identity or secret values. + +## JWKS rotation and outage + +On a new key generation: + +1. authenticate and validate the complete snapshot; +2. retain the stable verifier-policy identity when accepted semantics did not change; +3. publish the new generation witness atomically; +4. revalidate prepared evidence and direct leases that observe the generation change; and +5. invalidate evidence whose signing key was removed. + +A retained key may continue after successful revalidation. A key absent from the current authenticated snapshot cannot. Once a snapshot reaches its hard-validity deadline, unreadable current state denies even if a cache contains the old key. + +Alert on refresh failure, increasing snapshot age, unexpected key-set regression, incompatible algorithms, or invalid metadata. The base profile has no durable anti-rollback oracle: if an authenticated source republishes an old set, its keys may become current again. Deployments that require rollback prevention must add and monitor a separately authenticated monotonic version or durable key floor. Never fix an outage by extending hard validity without a reviewed policy change and a new exact-head claim. + +## Trusted-proxy secret rotation + +Create a new random HMAC version in the secret store and distribute it to the edge and verifier through authenticated channels. If overlap is necessary, bound it explicitly. Keep one replay namespace for the trusted-proxy domain and profile regardless of which active secret verified a nonce; retain the matched version only as private audit metadata. Exercise baseline, old-version, new-version, the same nonce re-signed across versions, cross-request, and direct-origin cases. + +Retire the old version only after its maximum timestamp skew, request lifetime, replay retention, and in-flight processing bounds have passed. Remove it from both edge and verifier and retain no secret value in logs or evidence. + +## Dependency incident + +When verifier, JWKS, binding, lifecycle, replay, policy, receipt, audit, relationship, resource, or invalidation state is unavailable or contradictory: + +1. keep the affected protected operations fail closed; +2. stop new leases and close leases whose current dependencies cannot be checked; +3. preserve aggregate availability signals without recording raw identity material; +4. restore the dependency from a consistent known-good point; +5. reconcile versions, receipts, replay retention, and application effects before reopening; and +6. rerun `FI-TRACE-DEPENDENCY-FAIL-CLOSED` plus affected state and privacy traces. + +Do not queue an authorization for later implicit approval. A client retry starts a new request with fresh evidence. + +## Backup and restore + +Back up a consistent authority set: + +- active bindings and immutable provenance; +- retired pairs, disabled identities, revoked keys, and pending lineage; +- typed history and monotonic versions; +- verifier policies, JWKS generation metadata, and domain-policy lineage; +- delegation and relationship revisions; +- request-bound receipts and required authorization audit evidence; +- replay state for every still-acceptable request window; and +- application idempotency state needed to reconcile receipts and effects. + +Keep secret material in the secret system's own protected backup process. Record secret versions, not values, with the authority backup. + +After restore, hold protected traffic closed. Verify referential and selector invariants, policy and key-generation freshness, replay coverage, receipt/application reconciliation, audit continuity, lease invalidation, and route inventory. Rerun the complete exact-head conformance matrix before advertising enforcement from a materially changed restore topology. + +Restoring an older database snapshot must not resurrect removed keys, deleted policy generations, retired pairs, disabled identities, revoked keys, or consumed pending lineage. Apply verified forward records or a privileged compensating transition before reopening. + +## Rollback + +An application rollback is safe only when the prior artifact understands the current authority schema, policy lineage, stable labels, and transition semantics. Pin the compatible artifact and verify its digest before rollout. + +If no compatible conformant artifact exists, disable discovery and fail the protected operation closed while repairing forward. Do not restore unsigned forwarded identity, a legacy corporate authority, Nostr-only access to an NIP-FI-protected route, an old JWKS key, or a prior database snapshot as an authorization shortcut. + +Any authority correction uses a new privileged transition with new receipt and history. Preserve the erroneous record and incident evidence. + +## Monitoring and privacy + +Monitor dependency readiness, public denial class, private stable denial reason, denial-observation drop and saturation rate, final-admission conflict, replay pressure, JWKS age, lease invalidation lag, lifecycle transition rate, receipt/application reconciliation, authorization-audit capacity, and route-inventory drift. + +Never use raw assertions, issuer-qualified identities, Nostr keys linked to private identity, email, display name, HMAC value, or private decision reason as a metric label or public trace field. Denial observations contain only a stable reason code, correlation identifier, timestamp, transport class, and bounded or keyed-hashed source coordinates; never raw tokens or verbatim unverified claims. Access-controlled investigation records retain only the minimum needed under a documented retention period. + +The denial-observation channel has a finite capacity independent of required authorization audit evidence. When it is unavailable or exhausted, drop or truncate the observation, preserve aggregate health signals where possible, and keep the denial effective without blocking or retrying admission. Do not use this best-effort history for authorization, lockout, or rate-limit decisions, and do not infer from a missing record that no denial occurred. + +Run privacy canaries through allowed and denied flows after adding an observability sink or changing redaction. Finding a canary in protocol output, public history, logs, metrics, or traces is an incident and a conformance failure. + +## Periodic exercises + +Rerun the full matrix for every immutable release tuple and after a material deployment, issuer, policy, proxy, storage, restore, or observability change. Periodically exercise concurrency, JWKS add/remove, proxy bypass/replay, dependency outage, recovery, restore, and rollback even when application code did not change. diff --git a/docs/NIP_FI_THREAT_MODEL.md b/docs/NIP_FI_THREAT_MODEL.md new file mode 100644 index 00000000000..488a7711377 --- /dev/null +++ b/docs/NIP_FI_THREAT_MODEL.md @@ -0,0 +1,136 @@ +# NIP-FI threat model + +This document records the security model for Buzz implementations of the normative [NIP-FI specification](nips/NIP-FI.md) and [formal model](nips/NIP-FI-MODEL.md). It is non-normative. The [conformance evidence matrix](nips/NIP-FI-CONFORMANCE.md) defines the behavioral release gate. + +## Current status + +This documentation revision contains no NIP-FI runtime adapter and establishes no conformance claim. The threats and mitigations below become implementation claims only when a later exact-head implementation and deployment tuple passes every applicable `FI-TRACE-*` behavior. + +## Security objectives + +NIP-FI adds issuer-qualified identity to fresh Nostr key proof without making either one sufficient by itself. Its security objectives are: + +- bind authority to one server-selected domain, route, operation, resource, and actor key; +- accept assertion input only under one provider-neutral assertion policy and current authenticated key or upstream-policy snapshot; +- require every trusted-edge profile to prove its provenance, protected request components, and declared replay semantics; +- keep durable identity/key bindings independent of assertion lifetime; +- preserve retirement, disablement, revocation, replacement, and provenance across ordinary authorization; +- keep preparation read-only and make final admission the only authority commit point; +- bound sessions and delegation by every current dependency; +- deny on unreadable, ambiguous, stale, or contradictory state; and +- prevent identity material and detailed decision reasons from becoming public or attacker-observable while retaining bounded private denial signals. + +## Protected assets + +| Asset | Required protection | +|---|---| +| Assertions and confidential revalidation handles | Confidentiality, integrity, bounded retention, and no public projection | +| Fresh Nostr proofs and proven actor keys | Request binding, freshness, and separation between keys on one connection | +| Trusted-proxy HMAC keys, timestamps, and nonces | Secret delivery, canonical request binding, replay retention, and rotation | +| Registered-profile contracts and deployment evidence | Integrity of authoritative field mapping, caller authentication, origin controls, request protection, deadlines, and profile-specific replay claims | +| Server-selected domain and request context | Integrity against host, forwarded-field, path, query, and body substitution | +| Assertion policy and authenticated snapshot generations | Stable semantic identity, authentic refresh, hard expiry, and current-key or upstream-policy revalidation | +| Binding and lifecycle state `B`, `T`, `X`, `Y`, `Q`, `H`, and `V` | Serialization, durability, monotonic lineage, and backup consistency | +| Prepared evidence, receipts, replay claims, and audit evidence | Integrity, idempotence, privacy, and atomic final admission | +| Denial-observation channel | Separate finite capacity, minimal attacker-controlled payload, drop visibility, and no authorization effect | +| Leases, delegation, and invalidation state | Finite bounds and current owner, key, policy, and resource dependencies | +| Conformance reports and artifacts | Exact-revision provenance, digest integrity, completeness, and reproducibility | + +## Actors and trust boundaries + +- **Client:** controls all request fields and may hold a valid assertion, a Nostr key, both, or neither. +- **Trusted edge:** terminates the external request, strips inbound authority-bearing fields, and produces the configured profile evidence. For `trusted-proxy-hmac-v2`, it creates the request-bound HMAC envelope. A registered authenticated-edge adapter instead depends on the reviewed caller-authentication, origin-isolation, request-integrity, and upstream-policy boundary. +- **Buzz ingress:** selects the domain and route from trusted configuration, verifies transport evidence, and invokes one final-admission authority. +- **Issuer or upstream-policy authority:** authenticates identity and publishes verification keys or policy snapshots. It is trusted only within configured policy and freshness bounds. +- **State services:** store binding, lifecycle, replay, receipt, audit, policy, relationship, and application state. +- **Operator:** controls deployment policy and privileged lifecycle transitions. Operator authority is not ordinary user authority. +- **Evidence runner:** drives production-equivalent behavior and retains privacy-safe conformance artifacts for one immutable claim tuple. + +The external client-to-edge, edge-to-origin, origin-to-issuer, process-to-storage, operator-to-lifecycle, and test-runner-to-deployment boundaries are independent. Trust at one boundary does not replace authentication at another. + +## Attacker capabilities + +The model assumes an attacker can: + +- send arbitrary HTTP and WebSocket traffic, headers, authority values, paths, query strings, bodies, events, and Nostr keys; +- reach any listener exposed by deployment, replay captured traffic, race enrollment and lifecycle operations, and hold connections across rotations; +- steal a bearer assertion without stealing its user's Nostr key, or steal a Nostr key without obtaining a valid assertion; +- choose equal subjects across issuers and collide inputs across domains; +- trigger issuer or upstream-policy, authenticated snapshot, database, replay, policy, receipt, audit, and network outages; and +- observe public responses and externally exported logs, metrics, and traces. + +The model does not assume that source IP, header presence, a private subnet, or a function name proves trusted provenance or authorization. + +## Threats and required evidence + +| Threat | Security effect | Required control | Behavioral evidence | +|---|---|---|---| +| Forged or unsigned forwarded identity | Assertion accepted without the configured trusted edge | Require the complete selected profile; reject direct, missing, mixed, and client-injected provenance without fallback | `FI-TRACE-PROXY-SPOOF` | +| Proxy nonce replay | Duplicate authorization or application effect | Retain nonce state through final admission and commit replay claim with receipt and authority state | `FI-TRACE-PROXY-REPLAY`, `FI-TRACE-FINAL-DENIAL-NO-MUTATION` | +| Cross-request HMAC reuse | Identity transplanted to another domain, peer, proof transport, or operation | MAC the timestamp, nonce, assertion digest, authorization domain, proof transport, authenticated client peer, method, authority, path/query, and body digest using exact canonical bytes | `FI-TRACE-PROXY-CROSS-REQUEST` | +| Inadequate registered-edge contract | Unchecked fields, a spoofed caller, origin bypass, or request tampering becomes authority | Close the assertion-policy mapping and profile contract; authenticate the immediate caller; isolate the accepting origin; strip inbound fields; protect every authorization-relevant component; validate upstream policy; and test the profile's actual replay surface | `FI-TRACE-PROXY-SPOOF`, `FI-TRACE-PROXY-REPLAY`, `FI-TRACE-PROXY-CROSS-REQUEST`, `FI-TRACE-VERIFIER-PARITY` | +| Domain or route confusion | Authority crosses tenants or bypasses policy | Resolve context from trusted listener and route state; reject uncovered or different-lineage authorities | `FI-TRACE-DOMAIN-SPOOF`, `FI-TRACE-AUTHORITY-UNIFORM`, `FI-TRACE-CROSS-DOMAIN-COLLISION` | +| Transport-specific verifier drift | A weaker ingress accepts assertion input or claims rejected elsewhere | Require one closed normalized-result and final-admission contract and run one corpus through every transport adapter | `FI-TRACE-VERIFIER-PARITY` | +| Verifier policy aliasing | A semantic change reuses an old policy identity | Digest every configured semantic input and a versioned compiled verifier-contract fingerprint | `FI-TRACE-VERIFIER-PARITY` policy vectors | +| Reassignable issuer subject | A different account inherits identity-scoped lifecycle or recovery authority | Treat stable non-reassignment as an issuer trust and deployment prerequisite; disable the policy when it cannot be established | Issuer review record; `FI-TRACE-CROSS-DOMAIN-COLLISION` | +| JWKS rotation race or stale key use | A key absent from the current snapshot continues to authorize, or a retained key fails unpredictably | Separate policy identity from JWKS generation and revalidate prepared evidence and leases on generation change | `FI-TRACE-JWKS-ADD`, `FI-TRACE-JWKS-REMOVE` | +| JWKS source rollback | Republishing an old authenticated set reauthorizes removed keys | Treat this as residual issuer risk unless the deployment adds an authenticated monotonic version or durable key floor | A→B→A `FI-TRACE-JWKS-REMOVE` evidence | +| Assertion/key substitution | Issuer identity attaches to an unproven key | Require fresh Nostr proof and equality with any asserted key claim | `FI-TRACE-ASSERTION-KEY-MISMATCH` | +| Binding takeover or concurrent first use | One identity or key silently replaces another | Serialize both sides of the partial bijection and commit at most one conflicting enrollment | `FI-TRACE-BINDING-CONFLICT`, `FI-TRACE-CONCURRENT-ENROLLMENT` | +| Tombstone bypass | Retired, disabled, revoked, or pending lineage reappears | Check every active lifecycle selector before enrollment and preserve lineage durably | `FI-TRACE-TOMBSTONE-REPLAY` | +| Assertion expiry confused with binding expiry | A durable binding disappears or is recreated with different provenance | Keep binding lifetime independent; treat administrative expiry as a separate privileged field | `FI-TRACE-ASSERTION-REFRESH`, `FI-TRACE-ADMIN-EXPIRY` | +| Stale prepared decision or time-of-check/time-of-use race | Changed request or authority state commits under old evidence | Keep preparation read-only; seal witnesses; reread applicable dependencies at final admission | `FI-TRACE-PREPARED-STALE`, `FI-TRACE-FINAL-DENIAL-NO-MUTATION` | +| Stolen assertion first use | Attacker enrolls its key for a new identity | Prefer attested or provisioned enrollment; expose TOFU only as explicit risk-labelled policy | `FI-TRACE-TOFU-THEFT` | +| Delegation survives owner change or expiry | Former or expired owner authority persists | Bind delegation to exact current owner version and a positive finite deadline | `FI-TRACE-DELEGATE-OWNER-ROTATED`, `FI-TRACE-DELEGATION-EXPIRED` | +| Cross-key session confusion | One key's lease authorizes another key | Key leases by actor, domain, capability, resource, and dependency set | `FI-TRACE-MULTI-KEY-SESSION` | +| Public decision oracle | Identity or private policy can be enumerated | Map private reasons to stable many-to-one public denial classes | `FI-TRACE-DENIAL-ORACLE` | +| Denial-observation exhaustion | Attacker-reachable denials consume finite observation capacity or interfere with admission | Use a separately bounded non-authoritative channel; keep denials effective when writes fail; alert on deny rate, drops, and saturation | `FI-TRACE-FINAL-DENIAL-NO-MUTATION`, `FI-TRACE-PRIVACY-NONPUBLIC` | +| Identity leakage | Assertions or claims enter public events or observability | Minimize private state and scan protocol output plus every configured sink with canaries | `FI-TRACE-PRIVACY-NONPUBLIC` | +| Dependency outage or contradictory state | Availability failure becomes an authorization bypass | Bound work, alert, and fail closed at preparation, final admission, and lease reuse | `FI-TRACE-DEPENDENCY-FAIL-CLOSED` | +| Evidence substitution | A report from another revision or transport contract is used to activate enforcement | Bind reports to one immutable claim tuple, transport-contract revision, and profile-contract digest and fail on missing or duplicate traces | Complete exact-head matrix validation | + +## Enrollment risk + +Attested enrollment resists bearer-assertion theft only when the issuer's key claim is itself trustworthy and equals the independently proven key. Provisioned enrollment moves creation to an explicit privileged transition but still requires target-key proof. TOFU accepts the first proven key for a never-enrolled identity and therefore cannot prevent first-use theft by an attacker holding a valid bearer assertion. + +Changing enrollment mode does not repair or reclassify existing bindings. Operators must use an authorized lifecycle transition and preserve the original provenance history. + +## Availability and resource exhaustion + +Fail-closed behavior deliberately trades availability for authorization safety. Attackers may amplify issuer refresh, signature verification, replay lookup, authorization-audit writes, denial observations, or policy reads. Implementations bound assertion and header sizes, canonicalization work, clock skew, JWKS refresh, replay retention, concurrency, queues, and observability work. A full or unavailable required authorization-audit or replay store denies instead of silently dropping evidence. + +The authorization-audit budget is a non-reclaimable installation-lifetime +capacity. Legitimate exhaustion is an accepted, unrecoverable, domain-wide +fail-closed outage within that installation and domain lineage. Sizing is an +irreversible installation-lifetime decision, so operators monitor consumption +and alert with substantial headroom. Successful or authorization-affecting +operations consume the finite budget; denied operations do not. The base +contract defines no prune, export, reset, acknowledgement, or recovery path. + +Denial observations have different failure semantics because a denial is already safe. They use finite capacity separate from the non-reclaimable authorization-audit budget. Saturation or write failure drops or truncates the observation, emits aggregate health signals where possible, and leaves the denial and authoritative stores unchanged. Records minimize attacker control: no raw tokens or verbatim failed claims, only stable reason and correlation identifiers, time, transport class, and bounded or keyed-hashed source coordinates. Authorization, lockout, and rate-limit policy does not consume this best-effort channel. + +Rate limits cannot replace cryptographic verification, lifecycle selectors, or final-admission serialization. + +## Residual risk + +The protocol cannot eliminate: + +- compromise or malicious behavior by an accepted issuer within its configured claims; +- issuer subject reassignment after operator review; +- authenticated JWKS source rollback when no monotonic anti-rollback extension is deployed; +- compromise of the trusted edge, an active HMAC key, a registered adapter or its authenticated caller, the Buzz process, storage credentials, or privileged operator authority; +- first-use theft in risk-labelled TOFU mode; +- denial of service caused by required dependencies failing closed; +- unrecoverable domain-wide outage after legitimate authorization-audit budget exhaustion; +- correlation visible to systems that legitimately process private identity state; or +- an implementation defect that the exact behavioral matrix does not exercise. + +Use short-lived and narrowly scoped secrets, separation of duties, access-controlled audit, independent artifact retention, and periodic reruns to reduce these risks. + +## Out of scope + +NIP-FI does not define provider setup, account recovery at an issuer, public identity projection, a public profile event, human-resources policy, or a mechanism for trusting ordinary unsigned corporate headers. A registered profile can rely on projected claims only under its complete cryptographically authenticated edge contract; field presence alone never creates authority. + +## Review triggers + +Review this threat model and rerun affected traces when a change adds an ingress, transport, issuer rule, accepted algorithm, claim, enrollment mode, lifecycle transition, delegation capability, state dependency, cache, lease, proxy hop, observability sink, restore process, or rollback path. diff --git a/docs/examples/nip-fi-conformance-report.json.example b/docs/examples/nip-fi-conformance-report.json.example new file mode 100644 index 00000000000..fb599b1971c --- /dev/null +++ b/docs/examples/nip-fi-conformance-report.json.example @@ -0,0 +1,213 @@ +{ + "schema": "nip-fi-conformance-report-v2", + "document_role": "shape-example-not-conformance-claim", + "numeric_values_role": "illustrative-shape-values-not-production-defaults", + "claim": { + "implementation_revision": "REPLACE_WITH_40_HEX_COMMIT", + "adapter_revision": "REPLACE_WITH_40_HEX_COMMIT", + "build_artifact_sha256": "REPLACE_WITH_64_HEX_DIGEST", + "deployment_revision": "REPLACE_WITH_IMMUTABLE_DEPLOYMENT_ID", + "domain_policy_sha256": "REPLACE_WITH_64_HEX_DIGEST", + "transport_contract_revision": 2, + "advertised_transport_profiles": [ + "trusted-proxy-hmac-v2" + ], + "configured_transport_profiles": [ + { + "profile_ref": "trusted-proxy-hmac-v2", + "profile_contract_artifact": "REPLACE_WITH_CONTENT_ADDRESSED_ARTIFACT_REFERENCE", + "profile_contract_digest": "sha256:REPLACE_WITH_64_HEX_DIGEST" + } + ], + "enrollment_mode": "attested-key", + "delegation": false + }, + "results": [ + { + "trace_id": "FI-TRACE-PROXY-SPOOF", + "transport_contracts": [ + { + "profile_ref": "trusted-proxy-hmac-v2", + "transport_contract_revision": 2, + "profile_contract_digest": "sha256:REPLACE_WITH_64_HEX_DIGEST" + } + ], + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-PROXY-REPLAY", + "transport_contracts": [ + { + "profile_ref": "trusted-proxy-hmac-v2", + "transport_contract_revision": 2, + "profile_contract_digest": "sha256:REPLACE_WITH_64_HEX_DIGEST" + } + ], + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-PROXY-CROSS-REQUEST", + "transport_contracts": [ + { + "profile_ref": "trusted-proxy-hmac-v2", + "transport_contract_revision": 2, + "profile_contract_digest": "sha256:REPLACE_WITH_64_HEX_DIGEST" + } + ], + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-AUTHORITY-UNIFORM", + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-VERIFIER-PARITY", + "transport_contracts": [ + { + "profile_ref": "trusted-proxy-hmac-v2", + "transport_contract_revision": 2, + "profile_contract_digest": "sha256:REPLACE_WITH_64_HEX_DIGEST" + } + ], + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-DOMAIN-SPOOF", + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-ASSERTION-KEY-MISMATCH", + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-BINDING-CONFLICT", + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-TOMBSTONE-REPLAY", + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-ASSERTION-REFRESH", + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-ADMIN-EXPIRY", + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-JWKS-ADD", + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-JWKS-REMOVE", + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-PREPARED-STALE", + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-FINAL-DENIAL-NO-MUTATION", + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-CONCURRENT-ENROLLMENT", + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-TOFU-THEFT", + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-DELEGATE-OWNER-ROTATED", + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-DELEGATION-EXPIRED", + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-DENIAL-ORACLE", + "execution": { + "iteration_count": 1000, + "warm_up_iterations": 100, + "runner_artifact_sha256": "REPLACE_WITH_64_HEX_DIGEST", + "environment_sha256": "REPLACE_WITH_64_HEX_DIGEST", + "public_response_corpus_sha256": "REPLACE_WITH_64_HEX_DIGEST", + "sampling_method": "REPLACE_WITH_PREDECLARED_METHOD_REFERENCE", + "predeclared_bounds": "REPLACE_WITH_IMMUTABLE_BOUNDS_REFERENCE", + "statistical_rule": "REPLACE_WITH_IMMUTABLE_RULE_REFERENCE", + "noise_treatment": "REPLACE_WITH_PREDECLARED_NOISE_TREATMENT", + "acceptance_threshold": "REPLACE_WITH_PREDECLARED_THRESHOLD", + "raw_measurements_sha256": "REPLACE_WITH_64_HEX_DIGEST", + "automatic_retry_on_failure": false + }, + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-DEPENDENCY-FAIL-CLOSED", + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-MULTI-KEY-SESSION", + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-CROSS-DOMAIN-COLLISION", + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + }, + { + "trace_id": "FI-TRACE-PRIVACY-NONPUBLIC", + "status": "not-run", + "adapter_test_ids": [], + "evidence": [] + } + ] +} diff --git a/docs/examples/nip-fi-recovery-plan.json.example b/docs/examples/nip-fi-recovery-plan.json.example new file mode 100644 index 00000000000..38e5bf9a140 --- /dev/null +++ b/docs/examples/nip-fi-recovery-plan.json.example @@ -0,0 +1,44 @@ +{ + "schema": "nip-fi-recovery-plan-v1", + "document_role": "operator-review-template-not-runtime-request", + "domain": "community.example.test", + "operation": "recover", + "authorization": { + "operator_authority_ref": "evidence://operator-approval/REPLACE_ME", + "request_digest": "REPLACE_WITH_64_HEX_DIGEST", + "expires_at": "2030-01-01T00:00:00Z" + }, + "identity_ref": "private://identity/REPLACE_ME", + "expected_lineage": { + "old_binding_version": "REPLACE_WITH_VERSION", + "retired_pair_ref": "private://retired-pair/REPLACE_ME", + "pending_replacement_ref": "private://pending-replacement/REPLACE_ME" + }, + "target": { + "nostr_key": "REPLACE_WITH_64_HEX_PUBLIC_KEY", + "fresh_proof_ref": "evidence://nostr-proof/REPLACE_ME", + "issuer_attestation_required": true, + "issuer_attestation_ref": "evidence://assertion/REPLACE_ME" + }, + "preconditions": [ + "identity-is-not-disabled", + "identity-has-no-active-binding", + "pending-lineage-matches", + "retired-pair-matches", + "target-key-is-unbound", + "target-key-is-not-revoked", + "target-pair-is-not-retired" + ], + "expected_postconditions": [ + "old-pair-remains-retired", + "pending-lineage-is-consumed-once", + "new-binding-has-new-version", + "replacement-provenance-matches-evidence", + "dependent-leases-are-invalidated" + ], + "rollback": { + "database_rewind_allowed": false, + "ordinary_authorization_repair_allowed": false, + "compensating_privileged_transition_required": true + } +} diff --git a/docs/examples/nip-fi-stock-domain.json.example b/docs/examples/nip-fi-stock-domain.json.example new file mode 100644 index 00000000000..c481ebe4f99 --- /dev/null +++ b/docs/examples/nip-fi-stock-domain.json.example @@ -0,0 +1,156 @@ +{ + "schema": "nip-fi-stock-domain-review-v2", + "document_role": "review-template-not-runtime-schema", + "numeric_values_role": "illustrative-review-values-not-production-defaults", + "domain": { + "id": "community.example.test", + "authorization_domain_id": "00000000-0000-0000-0000-000000000001", + "authority": "community.example.test:443", + "protected_ingress_classes": [ + "websocket", + "http-event-query-count", + "media-read-write", + "git-smart-http", + "audio", + "invite-moderation", + "operator-lifecycle" + ] + }, + "policy_control": { + "current_policy_identity": "REPLACE_WITH_STABLE_POLICY_DIGEST", + "current_policy_lineage": "REPLACE_WITH_MONOTONIC_LINEAGE_ID", + "verifier_contract_fingerprint": "REPLACE_WITH_COMPILED_CONTRACT_DIGEST", + "verifier_contract_fingerprint_source": "implementation-observed-not-operator-input" + }, + "transport": { + "profile": "trusted-proxy-hmac-v2", + "transport_contract_revision": 2, + "profile_contract_artifact": "REPLACE_WITH_CONTENT_ADDRESSED_ARTIFACT_REFERENCE", + "profile_contract_digest": "sha256:REPLACE_WITH_64_HEX_DIGEST", + "assertion_field": "Nostr-Federated-Identity", + "assertion_scheme": "Bearer", + "assertion_field_cardinality": "exactly-one", + "federated_assertion_in_authorization_field": false, + "provenance_field": "Nostr-Federated-Identity-Provenance", + "client_peer_field": "Nostr-Federated-Identity-Client-Peer", + "client_peer_encoding": "canonical-ip", + "hmac_secret_ref": "secret://nip-fi/community.example.test/proxy-hmac", + "hmac_algorithm": "HMAC-SHA-256", + "minimum_hmac_secret_bits": 256, + "maximum_provenance_age_seconds": 30, + "future_skew_seconds": 5, + "maximum_provenance_field_bytes": 512, + "minimum_nonce_bytes": 16, + "maximum_nonce_bytes": 64, + "nonce_retention_minimum_seconds": 35, + "nonce_replay_scope": "domain-and-profile-independent-of-secret-version", + "mac_binds": [ + "assertion-digest", + "authorization-domain-id", + "method", + "authority", + "path-and-query", + "body-digest", + "proof-transport-code", + "authenticated-client-peer" + ], + "legacy_v1_envelopes": "deny", + "direct_origin_allowed": false, + "fallback_profile": null + }, + "request_context": { + "method_source": "matched-server-route-uppercase-ascii", + "authority_source": "server-configured-host-and-explicit-port", + "path_and_query_source": "exact-post-routing-origin-form", + "body_digest": "SHA-256-over-exact-request-body", + "maximum_body_bytes": 10485760, + "unknown_operation": "deny", + "unknown_resource": "deny" + }, + "assertion_policy": { + "issuer": "https://issuer.example.test", + "audiences": [ + "community.example.test" + ], + "allowed_asymmetric_algorithms": [ + "RS256" + ], + "authenticated_key_source": "https://issuer.example.test/keys.json", + "subject_claim": "sub", + "subject_stability_evidence": "REPLACE_WITH_REVIEW_RECORD_ID", + "subject_reassignment_policy": "disable-issuer-until-remediated", + "nostr_key_claim": "nostr_pubkey", + "maximum_assertion_age_seconds": 300, + "clock_skew_seconds": 30, + "maximum_assertion_bytes": 16384, + "maximum_protected_header_bytes": 4096, + "maximum_claims_bytes": 8192, + "maximum_subject_bytes": 512, + "maximum_key_identifier_bytes": 256, + "maximum_configured_keys": 32, + "jwks_soft_refresh_seconds": 300, + "jwks_stale_known_key_seconds": 0, + "jwks_hard_maximum_age_seconds": 3600, + "jwks_rollback_model": "current-authenticated-snapshot-no-durable-anti-rollback", + "unknown_key_refresh_is_per_request": false + }, + "nostr_proof": { + "nip42_maximum_age_seconds": 60, + "nip98_maximum_age_seconds": 60, + "replay_retention_covers_acceptance_window": true + }, + "enrollment": { + "mode": "attested-key" + }, + "lifecycle_policy": { + "ordinary_enrollment_sets_binding_not_after": false, + "administrative_expiry_requires_privileged_transition": true + }, + "leases": { + "direct_maximum_seconds": 300, + "delegation_enabled": false, + "delegated_maximum_seconds": null + }, + "safety": { + "unknown_protected_route": "deny", + "dependency_unavailable": "deny", + "competing_authority": "startup-deny", + "public_identity_projection": false, + "denial_observation": { + "authority_role": "non-authoritative-best-effort", + "capacity_is_separate_from_authorization_audit": true, + "maximum_records": 100000, + "maximum_bytes": 268435456, + "maximum_record_bytes": 1024, + "write_failure_effect": "denial-stands-observation-may-drop", + "authorization_receipt_on_denial": false, + "allowed_payload": [ + "stable-reason-code", + "correlation-identifier", + "timestamp", + "transport-class", + "bounded-or-keyed-hashed-source-coordinates" + ], + "raw_tokens_or_verbatim_unverified_claims": "forbidden", + "policy_dependency": "none" + }, + "public_denial_mapping": { + "missing_evidence": { + "http_status": 401, + "nostr_prefix": "auth-required:" + }, + "evidence_rejected": { + "http_status": 403, + "nostr_prefix": "restricted:" + }, + "authorization_denied": { + "http_status": 403, + "nostr_prefix": "restricted:" + }, + "authorization_unavailable": { + "http_status": 503, + "nostr_prefix": "restricted:" + } + } + } +} diff --git a/docs/nips/NIP-FI-CONFORMANCE.md b/docs/nips/NIP-FI-CONFORMANCE.md new file mode 100644 index 00000000000..9a027d40bb0 --- /dev/null +++ b/docs/nips/NIP-FI-CONFORMANCE.md @@ -0,0 +1,138 @@ +# NIP-FI conformance evidence + +This document turns the stable trace identifiers in the normative [NIP-FI specification](NIP-FI.md#stable-conformance-labels) and [formal model](NIP-FI-MODEL.md#stable-conformance-traces) into a behavioral evidence contract. The specification and model remain normative. + +A document, source scan, compiled symbol, configuration key, or passing prose review does not prove conformance. A claim is valid only when executable adapters exercise the behavior at the exact implementation revision and preserve the evidence required below. + +## Claim unit + +A conformance claim names one immutable tuple: + +```text +(implementation revision, + adapter revision, + build artifact digest, + deployment revision, + domain-policy digest, + advertised stock transport profiles, + transport-contract revision, + configured transport-profile references and profile-contract digests, + enrollment mode, + delegation support) +``` + +Changing any element creates a new claim. Results from another tuple cannot be carried forward without rerunning the affected traces. A report includes all 24 `FI-TRACE-*` identifiers exactly once. + +The later implementation stack owns the executable adapters and exact commands. This documentation revision supplies no runtime adapter and closes no behavioral gate. + +## Evidence rules + +Each passing trace records: + +- the exact claim tuple; +- for proxy and verifier-parity traces, one or more selected profile references with their transport-contract revision and profile-contract digest; +- the immutable profile-contract artifact bytes or a content-addressed artifact reference whose SHA-256 reproduces each claimed profile-contract digest; +- a stable behavioral test ID and adapter entry point; +- the command, start and end times, exit status, and random seed when used; +- synthetic input or a privacy-safe digest of it; +- relevant before-and-after state, wire results, and lease or lifecycle versions; +- the expected oracle and observed result; +- artifact locations and SHA-256 digests; and +- cleanup status for synthetic identities, keys, and domains. + +Reports and artifacts containing a registered profile reference or other private deployment detail MUST remain access controlled and MUST NOT enter public reports, examples, discovery, or protocol output. + +Stateful tests use an isolated database or namespace. They inspect committed state after the operation instead of inferring state from a response. Concurrency tests record every contender and the single serialized outcome. Time-boundary tests use a controlled clock. Privacy tests inspect protocol output and the configured log, metric, and trace sinks. + +When an adapter evaluates denial timing, it freezes the sampling method, production-equivalent environment, warm-up, sample count, statistic, noise treatment, and acceptance threshold before execution. The report retains those values and the raw privacy-safe measurements. An undefined or post-selected threshold cannot pass a trace. + +`FI-TRACE-DENIAL-ORACLE` runs a fixed positive iteration count on a pinned, +isolated runner at the exact implementation and adapter head. Before the run, +the operator records the environment, public-response corpus, bounds, +statistical rule, noise treatment, and acceptance threshold. A threshold +breach fails the gate and MUST NOT trigger an automatic retry. The failure is +retained and investigated before a separately authorized new run produces new +evidence. + +The following do not satisfy a trace: + +- searching source, documentation, schemas, or binaries for a token; +- asserting that a route calls a named function; +- recording only a unit-test name without its execution result; +- using a mock to prove deployed network isolation; +- citing a pull request check from another revision; or +- marking a required trace as passed because the feature is configured. + +## Trace matrix + +| Trace ID | Required behavioral oracle | Minimum evidence | Normative references | +|---|---|---|---| +| `FI-TRACE-PROXY-SPOOF` | Direct ingress and any missing, mixed, client-supplied, malformed, or profile-inconsistent trusted-edge evidence deny before authority, with no fallback. HMAC-v2 additionally rejects v1, missing or repeated provenance or client-peer fields, malformed canonical encoding, and every clock or size violation. | Profile-specific deployed negatives from inside and outside the edge, caller-authentication and origin-control evidence, field stripping, protected-component integrity, responses, and no-mutation state diffs. HMAC-v2 retains v1 rejection; missing/repeated/comma/extra components; canonical and non-canonical IPv4, IPv6, and IPv4-mapped peer cases; non-canonical base64url; nonce lengths 15, 16, configured maximum, and maximum plus one; MAC lengths 31, 32, and 33; field maximum and maximum plus one; future-skew equality and excess; age-bound just-before and equality; overflow cases; and listener topology. | `FI-INV-04`, `FI-INV-05`, `FI-INV-14` | +| `FI-TRACE-PROXY-REPLAY` | The configured trusted-edge profile enforces its declared replay semantics. HMAC-v2 final admissions using one live nonce commit at most once, including when the nonce is re-signed by another concurrently active secret; preparation consumes nothing. A reusable JWT is not treated as single-use when the profile permits bearer reuse and requires fresh request-appropriate Nostr proof. | Profile-specific captured-artifact, expiry, Nostr-proof replay, and concurrency evidence. HMAC-v2 retains parallel same-secret and cross-secret overlap transcripts, domain/profile replay namespace, replay-store before/after state, committed receipt count, matched secret version as private audit metadata only, and retention deadline. | `FI-INV-08`, `FI-INV-09`, `FI-INV-14` | +| `FI-TRACE-PROXY-CROSS-REQUEST` | Changing any request component protected by the configured profile denies. HMAC-v2 changes to assertion, authorization domain, proof transport, authenticated client peer, method, authority, path/query, or body deny under the original MAC. | One valid baseline plus one mutation for every component the profile contract protects, with wire results. HMAC-v2 retains exact canonical byte fixtures for the `NIP-FI-PROXY-2` input and every bound-field mutation. | `FI-INV-04`, `FI-INV-05` | +| `FI-TRACE-AUTHORITY-UNIFORM` | Every protected ingress uses one current domain policy and final-admission authority. Uncovered, competing, and different-lineage paths fail closed. | Executed route inventory covering WebSocket and every protected HTTP class, policy identity observed per route, negative unknown-route case, and startup result for incompatible policy lineage. | `FI-INV-15` | +| `FI-TRACE-VERIFIER-PARITY` | After valid profile-specific handling, equivalent authenticated assertion input and policy produce the same authorization projection and final-admission decision at each controlled time on every transport. The same trace validates each profile's identity, revision, digest, `policy_id`, and deadlines; `FI-TRACE-PREPARED-STALE` validates changed revalidation dependencies. Every configured or compiled assertion-semantic change advances `policy_id`; authenticated snapshot-only rotation does not. The stock profiles accept the assertion only as exactly one `Nostr-Federated-Identity: Bearer ` field. | Shared authorization-projection corpus through every adapter with byte-for-byte projection comparison and controlled-time decisions; per-profile identity, revision, digest, `policy_id`, and deadline checks; deterministic vectors that mutate issuer or authenticated upstream-source identity, audience and algorithms where applicable, identity/key/claim-capability mapping, deadlines, normalization, size bounds, and compiled verifier or adapter contract one at a time; key or upstream-policy snapshot add/remove/order-only vectors; cross-process reproducibility; stock valid single-field cases; transport rejection of `Authorization` bearer assertions, missing/repeated/comma-combined fields, provenance on `client-attached`, and mixed profiles on WebSocket and HTTP; and registered-adapter proof that unchecked fields cannot enter the result. | `FI-INV-06`, `FI-INV-16` | +| `FI-TRACE-DOMAIN-SPOOF` | Client domain, host, or forwarded authority cannot replace the server-selected domain. | Multi-domain requests over each ingress, trusted-route observation, state diff proving no cross-domain mutation, and redacted denial. | `FI-INV-04`, `FI-INV-14` | +| `FI-TRACE-ASSERTION-KEY-MISMATCH` | An asserted key different from the proven key denies before mutation. | Valid assertion and proof fixture with unequal keys, decision capture, and complete authority-state diff. | `FI-INV-05`, `FI-INV-08` | +| `FI-TRACE-BINDING-CONFLICT` | A conflict on either side of the active partial bijection denies without replacement or provenance change. | Identity-side and key-side conflict cases, before/after binding rows, lifecycle selectors, and receipts. | `FI-INV-01`, `FI-INV-02`, `FI-INV-09` | +| `FI-TRACE-TOMBSTONE-REPLAY` | Fresh evidence cannot cross a retired pair, disabled identity, revoked key, or pending replacement. | Four selector cases plus selector-conflict fixtures, fresh evidence, redacted responses, and unchanged state. | `FI-INV-03`, `FI-INV-10`, `FI-INV-14` | +| `FI-TRACE-ASSERTION-REFRESH` | A fresh assertion authorizes the same eligible durable binding after the enrollment assertion expires. | Controlled clock before and after original expiry, unchanged binding version and provenance, new bounded lease, and original assertion rejection. | `FI-INV-02`, `FI-INV-11` | +| `FI-TRACE-ADMIN-EXPIRY` | Before administrative expiry may allow; equality and after deny. Time alone creates no tombstone or free coordinate. | Controlled-clock before/equal/after cases, binding and selector state, lease deadline, and explicit privileged restoration case. | `FI-INV-02`, `FI-INV-10`, `FI-INV-11` | +| `FI-TRACE-JWKS-ADD` | A new generation that retains the signing key revalidates prepared evidence and may authorize the unchanged binding. | Two key snapshots, generation witnesses, retained signing-key identity, exact assertion revalidation, and final result. | `FI-INV-06`, `FI-INV-07` | +| `FI-TRACE-JWKS-REMOVE` | Removing the signing key denies prepared evidence and active direct leases while it is absent from the current authenticated snapshot. Hard snapshot expiry also denies when current state is unreadable. Republishing an old set may accept that key again unless a separately claimed anti-rollback extension prevents it. | Prepared and leased cases across removal, hard-deadline boundary, refresh outage, lease closure, and unchanged binding; an A→B→A snapshot sequence proving the declared current-snapshot or anti-rollback behavior. | `FI-INV-07`, `FI-INV-11`, `FI-INV-14` | +| `FI-TRACE-PREPARED-STALE` | A changed request, claim or capability, authenticated policy snapshot, or other applicable decision witness cannot authorize from stale preparation. Equivalent concurrent enrollment may recompute as existing. | Mutations for request, normalized claims or capabilities, binding, lifecycle, mode, policy and authenticated snapshot, resource, delegation, relationship, replay, and invalidation witnesses at final admission; final state and recomputation evidence. | `FI-INV-08`, `FI-INV-09`, `FI-INV-14` | +| `FI-TRACE-FINAL-DENIAL-NO-MUTATION` | Denied preparation, local-policy denial, and final-admission denial create no authoritative mutation or authorization receipt. With the bounded denial channel available, each denial records a privacy-safe observation. With it unavailable or exhausted, the denial still stands and authoritative stores remain unchanged. | Complete before/after snapshots of binding, lifecycle, replay, receipt, authorization-audit, lease, capability, and application stores for each denial layer; zero denied receipts; available-channel reason/correlation observation; unavailable and capacity-exhausted channel cases; bounded payload and drop/saturation signals. | `FI-INV-08`, `FI-INV-09`, `FI-INV-13` | +| `FI-TRACE-CONCURRENT-ENROLLMENT` | Identical first uses converge on one binding version. Conflicting first uses commit at most one winner. | Barrier-synchronized identical and conflicting races, all results, proof of the serialized single-winner outcome, history count, and final binding. | `FI-INV-01`, `FI-INV-09` | +| `FI-TRACE-TOFU-THEFT` | Stolen-assertion first use denies in attested and provisioned modes. Only explicitly configured risk-labelled TOFU may create the attacker's proven key. | Same synthetic theft fixture under all three modes, discovery/config witness, provenance result, and no-mutation denials. | `FI-INV-05`, `FI-INV-10` | +| `FI-TRACE-DELEGATE-OWNER-ROTATED` | Rotation, retirement, disablement, key revocation, owner-binding version change, or relationship revision makes old delegation non-current. No authority transfers to a replacement key. | Delegated allow baseline; each dependency mutation between preparation and final admission and during lease reuse; exact owner and relationship versions; replacement-key non-inheritance; denial; and bounded closure time. | `FI-INV-10`, `FI-INV-12` | +| `FI-TRACE-DELEGATION-EXPIRED` | Missing finite configuration, delegation-expiry equality, owner administrative-expiry equality, and use after either bound deny. | Controlled-clock just-before/equal/after cases for both bounds, configuration omission case, delegate proof, exact owner version, and lease deadline. | `FI-INV-11`, `FI-INV-12`, `FI-INV-14` | +| `FI-TRACE-DENIAL-ORACLE` | Unknown identity, conflicts, tombstones, enrollment posture, and private-policy denials are not distinguishable on the public wire. | Public response corpus normalized by transport; fixed iteration count; pinned isolated runner and recorded environment; exact implementation and adapter head; status/prefix comparison; predeclared bounds, timing method, statistical rule, noise treatment, and threshold with raw measurements; private-detail scan; and proof that a breach failed without automatic retry. | `FI-INV-13` | +| `FI-TRACE-DEPENDENCY-FAIL-CLOSED` | An unreadable verifier or registered adapter, authenticated key or upstream-policy snapshot, binding, lifecycle, replay, policy, receipt, audit, or invalidation state never allows. | One injected outage per dependency at preparation, final admission, and lease reuse where applicable, including authenticated upstream-policy snapshot retrieval; results and state diffs. | `FI-INV-14` | +| `FI-TRACE-MULTI-KEY-SESSION` | A lease for one authenticated key cannot authorize another key on the same connection. | One connection with two keys, per-key operations, lease lookup evidence, and wire results before and after invalidation. | `FI-INV-05`, `FI-INV-11` | +| `FI-TRACE-CROSS-DOMAIN-COLLISION` | Equal subjects across issuers and equal pairs across domains remain distinct and inherit no authority. | Two issuers and two domains with controlled collisions, four state snapshots, and cross-use denials. | `FI-INV-01`, `FI-INV-04` | +| `FI-TRACE-PRIVACY-NONPUBLIC` | Assertions and private identity material never enter protocol output, public history, discovery, logs, metrics, or traces. | Seeded canary claims, successful and denied flows, scans of every configured sink, and access-control evidence for retained private state. | `FI-INV-13`; [NIP-FI privacy](NIP-FI.md#privacy); [model privacy](NIP-FI-MODEL.md#denial-and-privacy-model) | + +## Applicability + +Every report contains every trace ID. `pass` and `not-applicable` are the only claimable statuses. A blank, skipped, expected failure, or `not-run` result cannot support a claim. + +`not-applicable` needs a machine-readable reason and behavioral proof that the optional surface is absent: + +- proxy traces may be not applicable only when no trusted-edge profile is accepted, `trusted-proxy-hmac-v2` is not advertised, and executable absence cases reject every trusted-edge evidence shape; +- JWKS traces may be not applicable only when no local JWT/JWKS verifier is configured and executable evidence proves the implementation has no local JWKS surface; an authenticated upstream-policy adapter must instead exercise equivalent current-policy snapshot and revalidation behavior under `FI-TRACE-VERIFIER-PARITY` and `FI-TRACE-PREPARED-STALE`; +- TOFU may be not applicable only when risk-labelled TOFU is neither configurable nor advertised and executable absence cases show rejection; +- delegation traces may be not applicable only when delegation is disabled, omitted from discovery, and denied on every ingress; and +- every other trace is required for an enforcing deployment. + +An implementation that supports an optional surface must run its traces even when one deployed domain does not activate that surface. + +## Adapter contract + +The implementation stack must supply an adapter manifest at its exact head. The manifest maps each applicable trace to executable test IDs and commands. It also identifies required services, fixtures, fault injection, and deployed-boundary steps. + +Adapters must drive public or production-equivalent entry points. Storage helpers may inspect state and inject a dependency outage, but they cannot replace the operation under test. A route test that calls an internal authorization function without traversing the protected ingress does not satisfy route coverage. + +The adapter exits nonzero when: + +- a trace is absent or duplicated; +- the implementation, adapter, artifact, deployment, or policy digest differs from the claim tuple; +- a proxy or verifier-parity result has a different transport-contract revision, selected profile reference, or profile-contract digest; +- a required result is not `pass`; +- a `not-applicable` result lacks absence evidence; +- an evidence artifact is missing or its digest differs; or +- cleanup or privacy inspection is incomplete. + +The example [conformance report](../examples/nip-fi-conformance-report.json.example) contains every stable trace with `not-run` status. It is a shape example, not a conformance claim. + +## Release gate + +Before NIP-FI discovery or enforcement is activated, reviewers verify: + +1. The implementation stack supplies exact-head behavioral adapters for every applicable trace. +2. One immutable claim tuple passes the complete matrix. +3. The protected-ingress inventory has no uncovered or competing authority. +4. Trusted-edge deployments include mechanism-specific live bypass, mixed-profile, field-injection, replay, cross-request, and verifier-parity evidence. +5. Restore, rollback, lifecycle, and dependency-outage exercises have completed against production-equivalent storage. +6. Public and operational sinks pass the privacy canary inspection. + +Documentation review, source review, and static scans remain useful review inputs. They do not close any item in this release gate. diff --git a/docs/nips/NIP-FI-MODEL.md b/docs/nips/NIP-FI-MODEL.md index c47d2dad299..130487a7def 100644 --- a/docs/nips/NIP-FI-MODEL.md +++ b/docs/nips/NIP-FI-MODEL.md @@ -1,220 +1,553 @@ # Scope -This model specifies a relay or HTTP service authorizing a Nostr principal only when a valid federated identity assertion and a valid Nostr proof resolve to the same active identity-to-key binding. It models authorization, enrollment, revocation, and key rotation. It does not publish the federated identity on Nostr and does not make the identity provider a Nostr signing authority. +This model defines the state and transitions required by [NIP-FI](NIP-FI.md). It covers direct authorization, enrollment, lifecycle changes, leases, and delegation. It does not define an identity provider, storage schema, operator API, public identity projection, or application-specific admission policy. + +The model is transport-neutral except where transport is part of the authorization evidence. NIP-42 and NIP-98 prove control of a Nostr key. A federated assertion does not. + +# Terms + +- `D`: an authorization domain selected from authenticated server routing and configuration. +- `i = (iss, sub)`: an issuer-qualified federated identity returned by assertion validation. +- `k`: a 32-byte Nostr public key returned by Nostr-proof validation. +- `A`: the exact authoritative assertion input. For both stock profiles it is the compact-JWS bytes; a registered profile closes its private input in its profile contract and its result mapping in its assertion policy. +- `N`: a fresh Nostr proof for the current connection or HTTP request. +- `R_t`: the server-owned target context `(method, authority, path_and_query, body_digest, transport, operation, resource)`. +- `R`: `R_t` sealed with the actor key returned by Nostr-proof validation. +- `P_t`: the transport tuple `(profile, transport_contract_revision, profile_contract_digest)` derived from the same trusted configuration as `R_t`. +- `E`: the closed normalized verified assertion result returned by the configured profile. +- `C`: local admission policy and resource state for `(D, R)`. +- `now`: verifier time. -The model is transport-independent. A concrete NIP must separately define how an assertion reaches a verifier and how support is advertised. NIP-42 and NIP-98 remain the mechanisms for proving control of a Nostr key; a bearer assertion alone is never a Nostr proof. +Client fields, forwarded routing fields, assertion claims, and Nostr tags cannot select `D`, `R`, `C`, or the operation being admitted. -# Terms and domains +# Persistent state -- `D`: authorization domain chosen by the service (for example one relay tenant). Bindings never cross domains implicitly. -- `I`: federated principal, the tuple `(iss, sub)`. `iss` is the assertion's exact validated issuer identifier and `sub` is its exact non-empty subject string. A username, email, display name, or bare `sub` is not an identity key. -- `K`: 32-byte Nostr public key. -- `A`: federated assertion. -- `P`: Nostr proof authenticating key `k`, such as a valid NIP-42 AUTH event or NIP-98 event. -- `now`: verifier time. -- `B_D`: active binding relation in domain `D`, a partial bijection between `I` and `K`. -- `R_D`: durable history of revoked bindings. -- `mode(D)`: enrollment policy, either `attested-key`, `provisioned`, or `tofu`. +For each domain `D`, the service maintains: + +```text +B_D : active binding relation +T_D : set of retired (identity, key) pairs +X_D : set of disabled identities +Y_D : set of revoked keys +Q_D : pending-replacement lineage +H_D : immutable lifecycle history +V_D : monotonic binding and lifecycle versions +``` + +These are authoritative stores. A deployment may also maintain `O_D`, a separately capacity-bounded denial-observation channel. `O_D` is not an authorization witness: reads, writes, loss, truncation, or exhaustion of `O_D` cannot change a decision, receipt, lease, capability, replay claim, lifecycle transition, or application effect. + +An active binding is: + +```text +Binding = ( + domain, + identity, + key, + version, + provenance, // attested-key | provisioned | tofu + created_at, + binding_not_after? // optional administrative bound +) +``` + +`binding_not_after` is absent unless a separately authorized administrative action sets it. Assertion `exp`, `iat`, or maximum age never creates, renews, or extends this bound. Binding provenance is immutable. + +Pending replacement lineage identifies an exact old binding version and old pair. A recovery or re-enablement transition can consume it once. Time passage alone does not create lifecycle state. + +# Binding and lifecycle invariants + +The following labels are stable conformance references. + +**`FI-INV-01 — partial bijection.`** Active bindings are one-to-one within a domain: + +```text +forall i, k1, k2: + (i, k1) in B_D and (i, k2) in B_D implies k1 = k2 + +forall i1, i2, k: + (i1, k) in B_D and (i2, k) in B_D implies i1 = i2 +``` + +**`FI-INV-02 — durable binding.`** Assertion expiry does not remove, retire, or expire a binding. A fresh eligible assertion may authorize the same binding after an earlier assertion expires. + +**`FI-INV-03 — tombstone monotonicity.`** Ordinary authorization never removes an element from `T_D`, `X_D`, or `Y_D`, consumes `Q_D`, or recreates a retired pair. + +**`FI-INV-04 — server-owned context.`** Every allowed operation uses one server-resolved `D`, `R`, operation, resource, and actor. Unauthenticated input cannot replace any of them. + +**`FI-INV-05 — independent evidence.`** Direct authorization requires both a currently valid assertion and a fresh Nostr proof. The asserted key, when present, equals the proven key. + +**`FI-INV-06 — stable verifier policy.`** Verifier-policy identity changes when accepted assertion semantics change, but not when authenticated key or upstream-policy snapshot contents rotate. Snapshot rotation changes the applicable generation. + +**`FI-INV-07 — current-policy verification.`** A prepared result or lease cannot survive removal of the key or upstream policy that authenticated it. A generation change requires revalidation against the current authenticated snapshot before use. + +**`FI-INV-08 — read-only preparation.`** Preparation creates no binding, lifecycle fact, replay claim, receipt, lease, authorization audit evidence, publication, last-seen value, or application mutation. A denied preparation may attempt only the non-authoritative bounded observation defined below. + +**`FI-INV-09 — atomic final admission.`** Enrollment, replay claims, receipts, and authorization audit evidence commit only after complete final revalidation. A denied or failed final admission leaves no authority mutation and creates no authorization receipt; attempting a non-authoritative denial observation is outside this commit. + +**`FI-INV-10 — explicit lifecycle authority.`** Provisioning, retirement, disablement, revocation, rotation, recovery, re-enablement, and administrative-expiry changes occur only through their separately authorized transition. + +**`FI-INV-11 — evidence-bounded leases.`** A lease ends no later than every normalized authority, authenticated policy snapshot, proof, transport-provenance, delegation, local-policy, binding-administrative, and implementation bound on which it depends. + +**`FI-INV-12 — current-owner delegation.`** Delegation requires an authorization-eligible owner binding at its exact current version, a fresh delegate proof, capability intersection, and a positive finite deadline. + +**`FI-INV-13 — privacy-safe denial.`** Public rejection is a many-to-one class. It does not reveal an identity, key, claim, binding, tombstone, enrollment mode, key identifier, or private policy fact. + +**`FI-INV-14 — fail closed.`** Unreadable, ambiguous, stale beyond policy, or inconsistent assertion, key, binding, lifecycle, replay, policy, resource, receipt, or audit state cannot produce authority. + +**`FI-INV-15 — uniform authority.`** Every protected ingress in a domain uses the same current domain policy and final-admission authority. An uncovered route, competing authority, or different policy lineage makes enforcement unavailable and fails closed. + +**`FI-INV-16 — canonical verifier.`** Assertion acceptance semantics have one provider-neutral normalized-result contract. Transport adapters cannot weaken or fork that contract or final admission. + +# Assertion-policy model + +A JWT verifier policy has stable identity `policy_id` and contains at least: + +```text +VerifierPolicy = ( + exact_issuer, + accepted_audiences, + allowed_asymmetric_algorithms, + authenticated_key_source_identity, + subject_rules, + optional_key_claim_rules, + optional_authorization_claim_or_capability_rules, + time_and_skew_rules, + normalization_and_size_rules, + verifier_contract_fingerprint +) +``` + +The implementation supplies a versioned `verifier_contract_fingerprint` over compiled acceptance rules not otherwise represented in configured policy. `policy_id` is a deterministic digest of every field above. It excludes transport, JWKS bytes, key identifiers, key order, cache metadata, retrieval time, and JWKS generation. Any configured or compiled change to accepted assertion semantics changes `policy_id`; a key-only rotation does not. + +A registered non-JWT assertion policy analogously identifies the exact authenticated upstream-policy source, identity/key/claim mapping, time and size bounds, normalization rules, snapshot semantics, and compiled contract fingerprint. Any semantic change advances `policy_id`; rotating only an authenticated upstream-policy snapshot does not. + +Each accepted key snapshot has an opaque generation `g`. Effective addition, removal, or replacement of a verification key changes `g`. Generation order need not be meaningful outside one verifier instance; equality is sufficient for witness comparison. + +The stock profiles call `ValidateAssertion(A, D, now)`. A registered profile either calls the same verifier or validates its closed upstream assertion and authorization claim set before returning the same normalized result. Ordinary forwarded headers and unchecked adapter-local fields cannot enter `E`. -A binding record is: +`ValidateAssertion(A, D, now)` returns: ```text -Binding = (domain, identity, key, source, created_at, revoked_at?) -source = attested-key | provisioned | tofu +JwtAssertionEvidence = ( + identity, + asserted_key?, + current_authorization_claims_or_capabilities, + deadline, + policy_id, + jwks_generation, + verification_key_identity, + key_snapshot_hard_deadline, + assertion_digest, + confidential_revalidation_handle // yields the exact compact-JWS bytes +) ``` -`display_name`, email, and similar values may be stored as mutable metadata but are never part of binding identity or an authorization decision. +Validation succeeds only when the checks in NIP-FI all pass. The input is exactly one bounded compact JWS with unambiguous protected headers and claims. The algorithm is allowed and asymmetric, and key selection produces exactly one compatible key. Issuer, audience, time, bounded non-empty subject, and optional asserted-key checks then pass under the same verifier contract. -# Trust assumptions +Subject stability and non-reassignment are issuer trust and deployment assumptions, not mechanically verifiable assertion properties. The operator records authoritative evidence for those properties before enabling an issuer. If the issuer can reassign a subject, the same identity coordinate can inherit lifecycle or recovery authority and the policy remains disabled until separately authorized remediation establishes a non-reassignable coordinate. -1. The verifier has an authenticated configuration for each accepted issuer: issuer identifier, allowed signing algorithms, key source, accepted audience(s), and claim mapping. -2. TLS and/or a trusted ingress boundary prevents attackers from injecting or replacing assertions. A reverse-proxy assertion header is trusted only when untrusted clients cannot reach the verifier directly and all inbound copies of that header are stripped before the trusted proxy sets it. -3. The issuer protects its signing keys and assigns stable, non-reassignable `sub` values within an issuer. If an issuer reassigns a subject, the model cannot distinguish the people. -4. The Nostr signature primitive is unforgeable and the concrete Nostr proof is fresh and bound to the target relay or HTTP request. -5. Binding-state transactions are serializable with respect to the same domain, identity, or key. The implementation may realize this with locks and unique constraints. -6. The verifier's clock is sufficiently accurate for assertion and proof freshness checks. +Unknown, duplicate, incompatible, or absent-from-current-snapshot keys fail closed. Retrieval and refresh work is bounded and coalesced. A stale known key can be accepted only inside an explicit finite stale-known-key policy and never after the hard key-cache bound. -Compromise of an accepted issuer or trusted ingress can impersonate federated principals. It still cannot satisfy Nostr proof for an already-bound uncompromised key, and in `attested-key` mode it cannot bind an arbitrary key unless the compromised issuer also attests that key. Theft of an assertion alone cannot authorize an already-bound identity without control of the bound Nostr key. +For a JWT policy, final admission denies when the current verifier policy identity differs from the prepared identity. If the current generation differs from the generation in prepared evidence or a lease, the verifier revalidates the original assertion under the current snapshot. Revalidation must reproduce the same identity, asserted key, claims or capabilities, policy identity, and live time bounds. A key addition can therefore preserve valid evidence when the old key remains accepted; key removal denies evidence signed by the removed key while it remains absent from the current snapshot. -# Assertion validity +The base model compares against the currently authenticated snapshot and defines no durable JWKS anti-rollback state. Republishing a previously removed key set can make those keys current again. A deployment claiming rollback prevention adds a separately authenticated monotonic version or equivalent durable key floor and tests that extension explicitly. -Let `ValidateAssertion(A, C, now)` return either `(i, k_a?, exp)` or failure under issuer configuration `C`. +# Assertion transport model -It succeeds only if all of the following hold: +Trusted listener, route, and authorization-domain configuration selects exactly one `P_t` before protected traffic is accepted. A stock profile is exactly `client-attached` or `trusted-proxy-hmac-v2`. A deployment may instead select one private registered trusted-edge profile whose identifier matches `x---v`. The verifier does not infer a profile from attacker-controlled fields and does not fall back between profiles after missing, mixed, or rejected evidence. -1. the signature validates under a currently trusted key and an explicitly allowed asymmetric algorithm; -2. `A.iss` exactly equals the configured issuer identifier used to select that key; -3. at least one `A.aud` value exactly equals an audience configured for this service; -4. `exp` exists and `now < exp`, allowing only a bounded configured clock skew; -5. if present, `nbf <= now` and `iat` is not unreasonably in the future; -6. the configured subject claim is a non-empty string; -7. `i = (A.iss, A.subject)`; and -8. if a configured Nostr-key claim is present, it parses to exactly one 32-byte key `k_a` (hex on the wire; bech32 may be accepted only as an explicitly documented input normalization). +Every profile returns the same closed result: -Unknown issuers, key IDs, algorithms, claims, and validation failures fail closed. Key retrieval failure also fails closed. A verifier must bound key-cache lifetime and refresh behavior; it must not accept a token merely because parsing succeeded. +```text +NormalizedAssertionEvidence = ( + identity, + asserted_key?, + current_authorization_claims_or_capabilities, + authority_deadlines, // non-empty; every member is finite + policy_id, + transport_profile, + transport_contract_revision, + profile_contract_digest, + revalidation_dependencies +) +``` + +For a stock profile, `NormalizeJwtEvidence(JwtAssertionEvidence, P_t)` supplies this result and preserves the JWT evidence as revalidation dependencies. A registered adapter authenticates its closed upstream assertion and policy snapshot and returns the same shape. In either case, `policy_id` owns assertion-to-result semantics and the profile-contract digest owns transport and provenance semantics. + +The profile preserves the exact server-owned domain and request context, requires independent fresh Nostr proof, and feeds the same binding, lifecycle, invalidation, lease, local-policy, and final-admission rules. `authority_deadlines` is non-empty; comparison is overflow safe and equality is expired. The normalized result is closed; it is not a generic trusted-header map. + +The `client-attached` profile carries exactly one `Nostr-Federated-Identity: Bearer ` field and no assertion-provenance field. Missing, repeated, combined, malformed, or mixed-profile fields deny. + +## Trusted-edge provenance -# Nostr-proof validity +Every trusted-edge profile strips inbound copies of its authority-bearing fields, cryptographically authenticates the immediate edge, denies requests not attributable to that edge, protects each authorization-relevant request component, and imposes a positive finite provenance acceptance bound that expires at equality. It records one of two constructions in its profile contract: -`ValidateProof(P, target, now) = k` only when the applicable Nostr standard verifies the event ID and Schnorr signature, freshness, and target binding: +- request-bound signature or MAC evidence, with bounded atomic replay consumption for any single-use claim; or +- an authenticated-edge adapter with validated upstream policy, accepting-origin isolation, inbound-field stripping, and full integrity for authorization-relevant request components. -- NIP-42: kind, challenge, relay URL, and timestamp are valid; or -- NIP-98: kind, absolute request URL, HTTP method, timestamp, and payload hash when required are valid. +The second construction does not inherit HMAC-v2's application-verified request seal and therefore owes stronger deployment-boundary evidence. Reusable edge identity is not nonce replay protection. A JWT-based registered adapter may permit reuse of an unexpired JWT when each admission has a fresh request-appropriate Nostr proof; it cannot claim single-use JWT semantics without defining and proving them. -A service may define another proof profile only if it has equivalent signer-control, freshness, and target/replay binding. The key used for the authorization decision is the key returned by proof validation, never an unsigned request field or assertion display claim. +A registered identifier and its private fields are never advertised in NIP-11. Registration cannot change the normalized-result shape or final-admission rules. -# Binding invariant +### Stock HMAC-v2 provenance -For every domain `D`, active bindings are one-to-one: +The trusted proxy removes every inbound assertion, provenance, and client-peer field and supplies exactly one `Nostr-Federated-Identity: Bearer ` field, one `Nostr-Federated-Identity-Provenance` field, and one `Nostr-Federated-Identity-Client-Peer` field. The provenance value is exactly `v2...`; v1 is rejected. `timestamp` is canonical unsigned decimal without leading zeroes except `0`. `nonce` and `mac` are canonical unpadded base64url, decoding to at least 16 bytes and exactly 32 bytes respectively. Each nonce has at least 128 bits from a cryptographically secure random source. The client-peer value is a canonical IPv4 or RFC 5952 IPv6 address, with IPv4-mapped IPv6 encoded as IPv4. Configured finite field, client-peer, and nonce maxima apply before decoding, lookup, or replay storage. + +Let `LP(x)` be the eight-byte unsigned big-endian length of byte string `x`, followed by `x`. The stock MAC input is: ```text -∀ i, k1, k2: (i, k1) ∈ B_D ∧ (i, k2) ∈ B_D ⇒ k1 = k2 -∀ i1, i2, k: (i1, k) ∈ B_D ∧ (i2, k) ∈ B_D ⇒ i1 = i2 +"NIP-FI-PROXY-2" || +LP(timestamp) || LP(nonce) || LP(SHA256(A)) || +LP(D.opaque_16_byte_id) || +LP(R_t.method) || LP(R_t.authority) || LP(R_t.path_and_query) || +LP(R_t.body_digest) || LP(R_t.proof_transport_code) || LP(client_peer) ``` -Equivalently, an active identity has at most one key and an active key has at most one identity in a domain. +The secret has at least 256 bits. The parsed timestamp is an eight-byte unsigned big-endian Unix-seconds value in the MAC. The nonce uses its decoded bytes. The assertion and body digests are raw SHA-256 output. The domain identifier is the exact server-selected 16-byte value. Method is the exact uppercase ASCII endpoint method. Authority is the server-configured lowercase ASCII host, explicit effective port, and bracketed IPv6 when applicable. Path and query are the exact post-routing ASCII origin-form, with `/` for an empty path, the leading `?` on a query, preserved percent-encoding and parameter order, and no fragment. The proof-transport code is one byte: `0x01` NIP-42, `0x02` NIP-98, `0x03` Git smart-HTTP session, or `0x04` Blossom. Client peer is the exact canonical ASCII field. Ambiguous or non-canonical values deny. + +The profile accepts time only when `timestamp <= now + future_skew` and `now < timestamp + maximum_provenance_age`, with finite configured bounds and overflow-safe comparisons. Equality at the age bound is expired. The verifier reconstructs every request component from authenticated server state, verifies the MAC in constant time against a finite active-secret set, and rejects a committed nonce in the domain/profile replay namespace regardless of which secret matched. Header presence or network location is not provenance. An assertion on direct ingress or without a valid MAC is denied. + +A direct lease under this profile ends no later than `min(assertion deadline, timestamp + maximum_provenance_age)` and remains subject to every other applicable lease bound. + +The nonce is only claimed during final admission and retained through at least `timestamp + maximum_provenance_age`. An applicable proof replay identity is retained through its entire acceptance window. Preparation reserves neither and cannot cause later requests to fail. + +# Nostr-proof model -# Authorization and enrollment transition +`ValidateNostrProof(N, D, R_t, now)` returns `k` only when signature, event identity, freshness, and exact target binding pass: -Given domain `D`, assertion result `(i, k_a?, exp)`, and proof result `k`, evaluate one atomic transaction: +- NIP-42 binds the proof to the current challenge, relay URL, connection, and freshness window. +- NIP-98 binds the proof to the exact server-resolved URL, method, payload digest when required, and freshness window. + +The key used for authorization is always `k`, never an assertion claim or unsigned input. Applicable proof replay identity is claimed only during final admission. + +# Prepared authorization + +A prepared result is immutable evidence, not authority: ```text -Authorize(D, i, k_a?, k): - if k_a exists and k_a != k: +PreparedAuthorization = ( + exact_context, // D, R_t, R, operation, resource, actor + nostr_proof_evidence, + path_dependencies, // direct: DirectPrepared | + // delegated: DelegatedPrepared + policy_and_resource_witness, + proposal, // existing | enroll | delegated + all_deadlines, + invalidation_dependencies +) + +DirectPrepared = ( + assertion_evidence, + transport_evidence, + actor_binding_and_lifecycle_witness, + enrollment_mode_witness +) + +DelegatedPrepared = ( + delegation_evidence, + owner_binding_and_lifecycle_witness, + relationship_witness +) +``` + +For direct authorization, preparation is equivalent to: + +```text +PrepareDirect(request, assertion_input, N): + (D, R_t, operation, resource) := ResolveTargetContext(request) or DENY + (e, transport_evidence) := + ValidateConfiguredTransport(D, R_t, assertion_input, P_t, now) or DENY + k := ValidateNostrProof(N, D, R_t, now) or DENY + R := SealActor(R_t, k) + i := e.identity + + if e.asserted_key exists and e.asserted_key != k: DENY(key_mismatch) - b_i := active binding in B_D for i, if any - b_k := active binding in B_D for k, if any + atomically read B_D(i), B_D(k), T_D(i,k), X_D(i), Y_D(k), + Q_D(i), mode(D), C, and all versions - if b_i = (i, k) and b_k = (i, k): - ALLOW(existing) + if i in X_D: DENY(identity_disabled) + if k in Y_D: DENY(key_revoked) + if (i,k) in T_D: DENY(pair_retired) + if Q_D(i) exists: DENY(explicit_replacement_required) - if b_i exists or b_k exists: + if B_D(i) = B_D(k) = b(i,k): + if b.binding_not_after exists and now >= b.binding_not_after: + DENY(binding_expired) + proposal := existing(b.version, b.provenance) + else if B_D(i) exists or B_D(k) exists: DENY(binding_conflict) - - switch mode(D): - attested-key: - if k_a is absent: DENY(key_attestation_required) - atomically insert (i, k, attested-key) into B_D - ALLOW(created) - provisioned: + else if mode(D) = attested-key: + require e.asserted_key = k + proposal := enroll(i, k, attested-key) + else if mode(D) = provisioned: DENY(binding_required) - tofu: - atomically insert (i, k, source = k_a exists ? attested-key : tofu) into B_D - ALLOW(created) + else if mode(D) = tofu: + provenance := e.asserted_key = k ? attested-key : tofu + proposal := enroll(i, k, provenance) + + EvaluateEveryLocalAdmissionPolicy( + D, R, operation, resource, k, + e.current_authorization_claims_or_capabilities + ) or DENY + return PreparedAuthorization(evidence, proposal, witnesses, deadlines) ``` -If a concurrent attempt finds the identical committed binding, it allows as `existing`; if the committed outcome cannot be read or storage is unavailable, deny — never fall back to an unchecked allow. The check and possible insertion must be linearizable for `(D, i, k)`. +Preparation is read-only for an existing binding and every enrollment mode. It creates no authoritative state. A denied preparation may attempt a non-authoritative denial observation only after the decision is fixed; observation failure cannot change or delay the denial. + +# Final admission -The resulting authorization lease is: +`CommitAdmission(prepared, current_request)` is equivalent to: ```text -L = (D, i, k, binding_version, expires_at) -expires_at <= assertion.exp +require ExactContextMatch(prepared, current_request) +require every evidence and policy deadline is live + +if prepared.path_dependencies is DirectPrepared: + require CurrentTransportContract(D, prepared.R_t) = + (prepared.direct.assertion_evidence.transport_profile, + prepared.direct.assertion_evidence.transport_contract_revision, + prepared.direct.assertion_evidence.profile_contract_digest) + require CurrentAssertionPolicyIdentity( + D, prepared.direct.assertion_evidence.identity.iss + ) = prepared.direct.assertion_evidence.policy_id + if any prepared profile or assertion-policy revalidation dependency changed: + revalidate the authoritative assertion input under current dependencies + require the normalized result, including identity, asserted key, + claims or capabilities, policy identity, and live deadlines, + is equivalent +else: + require prepared.path_dependencies is DelegatedPrepared + revalidate its delegation, relationship, owner, target, and policy witnesses + +atomically: + reread every applicable binding, lifecycle, enrollment-mode, policy, resource, + replay, receipt, and invalidation witness + recompute the complete decision from current state + require the current result is equivalent and eligible + require every applicable transport and proof replay identity is unclaimed, + including the HMAC-v2 nonce when that profile is selected + claim applicable replay identities + create the proposed binding only if it remains eligible + append the request-bound authorization receipt and required audit evidence + +return CommittedAuthorization( + exact_actor, + binding_dependencies, + capabilities, + dependencies, + deadline +) ``` -An implementation may impose a shorter maximum lease. A lease authorizes only policy-selected operations in `D`; it does not authorize signing and does not imply that event authors may differ from `k`. +The atomic section either commits all authority mutations or none. Unreadable state denies. Changed state requires complete recomputation and may commit only a semantically equivalent current decision. A concurrent identical enrollment may be reread and recomputed as `existing`; a conflicting enrollment denies. A missing or unreadable committed result does not fall back to allow. + +A denied or failed final admission creates no authorization receipt. After rollback, it may attempt a denial observation in `O_D`; that attempt is not part of the authoritative transaction. + +The application operation runs only after committed authorization. When it cannot share the authorization transaction, a request-bound idempotent receipt or equivalent staging prevents the same proof from creating a second effect. -# Session behavior +# Enrollment modes -For a single HTTP request, the assertion, Nostr proof, and authorization decision apply only to that request. +- `attested-key`: a first binding requires `asserted_key = proven_key`; provenance is `attested-key`. +- `provisioned`: ordinary authorization never creates a binding; only `ProvisionBinding` can. +- `tofu`: eligible first use may bind a proven key without issuer key attestation. This mode is explicitly risk-labelled because a stolen assertion for a never-enrolled identity can bind an attacker's key. A matching claim records `attested-key` provenance. -For a NIP-42 WebSocket connection, a relay may cache `L`, but it must not use the lease after `expires_at`. It must reject protected operations or terminate the connection; obtaining a fresh assertion and proof requires a new connection under this transport profile. A relay that learns that the binding or federated session was revoked must invalidate matching leases. Implementations must document their maximum revocation-detection latency; they cannot claim immediate revocation if they only poll. +A mode change affects future creation only. It cannot rewrite an existing binding or its provenance. -If multiple keys authenticate on one NIP-42 connection, authorization is tracked independently per key. A lease for one `(i, k)` must not authorize another authenticated key. +# Lifecycle transitions -# Revocation and rotation +Each lifecycle transition requires separate privileged authority bound to `D`, the transition, identity, old binding version when present, target key when present, and request. It atomically rechecks relevant `B_D`, `T_D`, `X_D`, `Y_D`, `Q_D`, policy, and version state; appends `H_D`; advances `V_D`; and invalidates dependent leases after commit. -Revocation is an explicit administrative transition: +`TargetEligible(i, k, allow_disabled)` means `k` is not in `Y_D`, `(i,k)` is not in `T_D`, neither side has an active binding, and `i` is not in `X_D` unless `allow_disabled` is true for ReenableIdentity. Provision and rotation additionally require no pending lineage. Recovery and re-enablement require the exact lineage stated by their transition. + +`ReplacementProvenance(evidence)` records the evidence that authorized the new target: `attested-key` only for a current matching issuer key attestation, otherwise `provisioned` for a privileged lifecycle transition. TOFU provenance is created only by ordinary first use in `tofu` mode and is never inherited by a replacement key. ```text -Revoke(D, i, k): - require (i, k) ∈ B_D - atomically remove (i, k) from B_D - append immutable revocation record to R_D - invalidate cached leases for the binding as soon as observed +ProvisionBinding(i, k): + require mode(D) = provisioned + require TargetEligible(i, k, false) and Q_D(i) is absent + require fresh target-key proof and any required issuer attestation + create Binding(i, k, new_version, provisioned) + +RetirePair(i, k, old_version): + require exact current Binding(i, k, old_version) + remove it from B_D + add (i,k) to T_D + record Q_D(i) for old_version + +DisableIdentity(i): + add i to X_D + if an active binding exists, retire its pair and record Q_D(i) + +RevokeKey(k): + add k to Y_D even when k is inactive + if an active binding exists, retire its pair and record Q_D(i) + repeated authorized application is idempotent and preserves lineage + +Rotate(i, k_old, old_version, k_new): + require exact current Binding(i, k_old, old_version) + require k_new is not revoked and (i,k_new) is not retired + require k_new has no active binding and Q_D(i) is absent + require fresh target-key proof and any required issuer attestation + remove (i,k_old) from B_D and add (i,k_old) to T_D + create Binding(i, k_new, new_version, ReplacementProvenance(evidence)) + +Recover(i, pending_version, k_new): + require exact Q_D(i, pending_version) + require i is not disabled + require TargetEligible(i, k_new, false) + require fresh target-key proof and any required issuer attestation + consume Q_D(i, pending_version) + create Binding(i, k_new, new_version, ReplacementProvenance(evidence)) + +ReenableIdentity(i, pending_version?, k_new): + require i in X_D + require exact absent lineage or exact Q_D(i, pending_version) + require TargetEligible(i, k_new, true) + require fresh target-key proof and any required issuer attestation + remove i from X_D + consume supplied lineage when present + create Binding(i, k_new, new_version, ReplacementProvenance(evidence)) + +SetAdministrativeExpiry(i, k, old_version, binding_not_after?): + require exact current Binding(i, k, old_version) + require separate privileged expiry authority + create the same pair and provenance at new_version with the supplied bound ``` -An assertion, including one with `k_a = k`, must not silently reactivate the same revoked binding unless the domain's explicit recovery policy authorizes that transition. This prevents replay of a still-valid assertion from undoing revocation. +`ReenableIdentity` deliberately clears `X_D` only while creating an eligible +target binding under fresh proof. Clearing disabled state without a target +would create a resurrection window: provisioned mode would have no target to +match, while TOFU could let the next ordinary admission capture first use. An +operator that wants to re-enable now and provision later leaves the identity +disabled until the target and proof are available; there is no separate +clear-only transition. + +Rotation does not globally revoke `k_old`; revocation does. A retired pair remains retired after rotation, recovery, or re-enablement. Ordinary authorization cannot cross disabled, revoked, retired, pending, or administratively expired state. + +# Lease model -Key rotation is not an authorization side effect: +HTTP authorization applies to one exact request and has no reusable lease. + +A WebSocket lease is: ```text -Rotate(D, i, k_old, k_new): - require explicit recovery/admin authorization - require (i, k_old) ∈ B_D - require no active binding for k_new - if issuer-attested rotation is required, require fresh k_a = k_new - atomically revoke (i, k_old) and create (i, k_new) - invalidate leases for k_old +Lease = ( + D, + actor_key, + binding_dependencies, // direct actor | delegated owner + lifecycle_versions, + evidence_dependencies, // DirectEvidence | DelegatedEvidence + operations, + resources, + deadline, + invalidation_dependencies +) ``` -A normal request that presents `i` with `k_new` while `k_old` is active is a conflict and must not rotate automatically. +`DirectEvidence` records the normalized result, profile and assertion-policy revalidation dependencies, and a confidential handle for the authoritative assertion input. JWT dependencies include JWKS generation, verification-key identity, the key-snapshot hard-validity deadline, assertion digest, and exact compact-JWS bytes. `DelegatedEvidence` records the exact owner binding and version, relationship identifier and revision, and delegation expiry. Revalidation material is retained only through the admission or lease that may need it and is destroyed on expiry, close, or invalidation. + +Before each protected use, the service verifies the key, domain, capability, resource, applicable binding and lifecycle versions, administrative bound, and deadline. Direct evidence requires current profile and assertion-policy dependencies; a changed dependency requires revalidation that reproduces the equivalent normalized result. For JWT evidence, the key snapshot must be readable before its hard-validity deadline and a changed JWKS generation requires revalidation of the original assertion. Delegated evidence requires the exact current eligible owner binding and relationship revision. A lease for one key never authorizes another key on the same connection. + +For direct authorization: + +```text +lease.deadline <= min( + every_normalized_authority_deadline, + proof_or_connection_bound, + transport_provenance_bound_if_present, + binding_not_after_if_present, + local_policy_bound, + implementation_maximum +) +``` + +For JWT evidence, the normalized authority deadlines include the assertion and key-snapshot hard-validity deadlines. The set is non-empty, comparison is overflow safe, and equality is expired. + +Lease expiry removes session authority. It does not remove, renew, or retire the durable binding. -# Delegation +# Delegation model -Delegation is outside the base identity-binding primitive. A separate delegation standard may allow a bound owner key to authorize a delegate key. If supported, the verifier must first validate the delegation proof and derive the owner key, then require an active, unexpired authorization lease or binding for that owner. It must not create a federated identity binding for the delegate unless explicitly specified. Delegation expiry/revocation and allowed operations remain bounded by both the owner identity authorization and the delegation. +Delegation is a separate evidence path. The delegate supplies fresh proof of `k_delegate`; both federated-assertion and assertion-provenance fields are absent. Separately validated evidence contains: + +```text +DelegationEvidence = ( + D, + owner_key, + delegate_key, + relationship_id, + relationship_revision, + audience, + operations, + resource_or_target, + not_before?, + mandatory_expiry +) +``` + +Preparation and final admission both require the exact current, authorization-eligible owner binding and version. The proven delegate key must equal `delegate_key`. The admitted capability is the intersection of delegation evidence and current local policy. The path cannot create or change an owner or delegate binding, identity, provenance, lifecycle fact, or last-seen state. + +The delegated deadline is bounded by the delegation expiry, delegate proof, current owner administrative bound and lifecycle version, local policy, configured positive finite delegated maximum, and any stronger owner evidence a deployment requires. Rotation makes the former owner key non-current, so its delegations deny and do not transfer to the new key. + +# Denial and privacy model + +Internal reasons map many-to-one to these public classes: + +```text +missing_evidence -> 401, auth-required: +evidence_rejected -> 403, restricted: +authorization_denied -> 403, restricted: +authorization_unavailable -> 503, restricted: +``` -# Safety properties +Internal reason, issuer, subject, key, binding existence, lifecycle state, enrollment mode, claim value, and key identifier remain private. Raw bearer material never enters protocol output, public events, logs, metrics, traces, or denial observations. -Under the trust assumptions, for direct (non-delegated) authorization: +Every denial attempts one access-controlled observation in `O_D` containing only a stable private reason code, correlation identifier, timestamp, transport class, and bounded or keyed-hashed source coordinates. Verbatim unverified claims are excluded. `O_D` has finite capacity independent of the non-reclaimable authorization-audit budget. If it is unavailable or exhausted, the denial stands without retry, blocking, latching, receipt creation, or authoritative mutation. Missing observations are not evidence of no denial. `O_D` may support monitoring and investigation, but authorization, lockout, and rate-limit decisions do not read it. -1. **Proof possession:** every allowed protected operation is associated with a valid proof of control of its Nostr key. -2. **Federated authenticity:** every allowed protected operation is associated with a currently valid assertion for its issuer-qualified identity. -3. **Agreement:** if the issuer supplies a key claim, the asserted key, proven key, and bound key are equal. -4. **Binding consistency:** no two active identities share a key and no identity has two active keys in one domain. -5. **No implicit rotation:** conflicting assertions or proofs cannot replace an active binding. -6. **Domain separation:** authorization in one domain does not imply authorization in another. -7. **Lease boundedness:** no cached authorization survives assertion expiry; after revocation is observed, no matching cached authorization remains valid. -8. **Fail-closed storage and verification:** validation, key retrieval, or binding-state failures never produce allow. -9. **Privacy:** conforming protocol behavior need not publish `iss`, `sub`, JWTs, email, or display names in Nostr events or relay-visible event history. +# Liveness -# Liveness properties +Liveness assumes available issuer keys, verifier policy, binding and lifecycle storage, replay storage, local policy, receipt and audit storage, and network: -Assuming the issuer, key source, binding store, and network are available: +1. An eligible existing binding with current evidence is eventually admitted. +2. An eligible unbound pair is eventually admitted exactly once when its enrollment mode permits creation. +3. After an authorized lifecycle transition and bounded invalidation, stale authority is denied and an eligible new binding can be admitted. -1. a valid assertion and matching proof for an existing active binding are eventually authorized; -2. an unbound pair is eventually authorized exactly once when the configured enrollment mode permits it; -3. after an authorized revocation/rotation and bounded cache invalidation, the old key is denied and the new valid binding can be authorized. +No liveness promise overrides `FI-INV-14`. Dependency outage may deny otherwise valid work. -Liveness is intentionally not guaranteed during issuer/JWKS/storage outage; availability must not override identity safety. +# Stable conformance traces -# Representative attack traces +Each trace identifier has the same meaning in NIP-FI, this model, and later executable conformance tests. Proxy and verifier-parity evidence carries the transport-contract revision and profile-contract digest. Evidence from another revision, profile, digest, adapter, deployment, or policy tuple is not transferable. Each profile declares the spoof, replay, and cross-request artifacts its oracle exercises. -| Trace | Required result | +| ID | Setup and 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 | -| Fresh assertion for a revoked pair | Deny unless explicit recovery transition authorizes reactivation | -| 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 | - -# Conformance hooks for the NIP - -The normative NIP should expose enough information for clients and operators to determine: - -- accepted assertion transport profile(s); -- issuer discovery or configured issuer and accepted audience rules without leaking private tenant data; -- whether a key claim is required; -- enrollment mode (`attested-key`, `provisioned`, or explicitly risk-labeled `tofu`); -- authorization lease/re-authentication behavior; -- machine-readable rejection classes using existing NIP-42 `auth-required:` and `restricted:` prefixes where applicable; -- privacy requirements and trusted-proxy deployment requirements. - -It should not standardize database schema, lock mechanism, Okta-specific claims, mutable display metadata, or an administration API. Those are implementation choices as long as the invariants and transitions above hold. +| `FI-TRACE-PROXY-SPOOF` | A trusted-edge request without the configured profile's valid provenance, including direct ingress, denies. HMAC-v2 retains its exact field, MAC, and boundary negatives. | +| `FI-TRACE-PROXY-REPLAY` | The configured trusted-edge profile enforces its declared replay semantics. For HMAC-v2, two final admissions using the same proxy nonce produce at most one committed authorization and preparation consumes neither. | +| `FI-TRACE-PROXY-CROSS-REQUEST` | Changing a request component protected by the configured profile denies. HMAC-v2 protects the assertion, domain, proof transport, authenticated client peer, method, authority, path/query, and body. | +| `FI-TRACE-AUTHORITY-UNIFORM` | Every protected ingress uses the same current domain policy and final-admission authority; uncovered, competing, or different-lineage paths fail closed. | +| `FI-TRACE-VERIFIER-PARITY` | Equivalent authenticated assertion input and policy produce the same authorization projection and final-admission decision at each controlled time on every transport. The same trace validates each profile's identity, revision, digest, `policy_id`, and deadlines; `FI-TRACE-PREPARED-STALE` validates changed revalidation dependencies. Policy vectors cover every configured and compiled semantic input and authenticated snapshot-only rotation. | +| `FI-TRACE-DOMAIN-SPOOF` | Client-selected domain or forwarded authority cannot replace server-owned context and denies on mismatch. | +| `FI-TRACE-ASSERTION-KEY-MISMATCH` | An asserted key different from the proven key denies before mutation. | +| `FI-TRACE-BINDING-CONFLICT` | A valid identity and key that conflict with either side of the active relation deny without replacement. | +| `FI-TRACE-TOMBSTONE-REPLAY` | A fresh assertion for a retired pair, disabled identity, revoked key, or pending replacement denies ordinary authorization. | +| `FI-TRACE-ASSERTION-REFRESH` | A fresh assertion can authorize the same eligible durable binding after the assertion used at enrollment expires. | +| `FI-TRACE-ADMIN-EXPIRY` | A fresh assertion after `binding_not_after` denies; only an explicit privileged transition can restore access. | +| `FI-TRACE-JWKS-ADD` | Generation changes, the old signing key remains accepted, revalidation passes, and the unchanged binding may authorize. | +| `FI-TRACE-JWKS-REMOVE` | Generation changes, the signing key is removed, and prepared evidence and leases signed by it deny. | +| `FI-TRACE-PREPARED-STALE` | A request, binding, lifecycle, policy, resource, mode, replay, or invalidation witness changes before final admission and the stale decision denies or is completely recomputed. | +| `FI-TRACE-FINAL-DENIAL-NO-MUTATION` | Denied preparation, local policy, and final admission create no authoritative mutation or authorization receipt. An available denial channel records one bounded observation; an unavailable or exhausted channel leaves the denial and authoritative stores unchanged. | +| `FI-TRACE-CONCURRENT-ENROLLMENT` | Identical eligible first uses converge on one binding version; conflicting first uses commit at most one winner. | +| `FI-TRACE-TOFU-THEFT` | Stolen assertion first use denies in attested and provisioned modes; only explicit risk-labelled TOFU may bind the attacker's proven key. | +| `FI-TRACE-DELEGATE-OWNER-ROTATED` | Owner rotation makes an old-owner delegation non-current and denies without inheritance. | +| `FI-TRACE-DELEGATION-EXPIRED` | Missing or expired finite delegation bounds deny. | +| `FI-TRACE-DENIAL-ORACLE` | Unknown, conflict, tombstone, and private-policy denials are not publicly distinguishable. | +| `FI-TRACE-DEPENDENCY-FAIL-CLOSED` | Unreadable current verifier, key, state, replay, policy, receipt, or audit dependency denies. | +| `FI-TRACE-MULTI-KEY-SESSION` | A lease for one authenticated key does not authorize another key on the same connection. | +| `FI-TRACE-CROSS-DOMAIN-COLLISION` | Equal `sub` values across issuers or equal pairs across domains remain distinct and cannot inherit authority. | +| `FI-TRACE-PRIVACY-NONPUBLIC` | Assertion and private identity material in protocol output, public history, or observability is a conformance failure. | # Sources -- NIP-42 authentication: https://github.com/nostr-protocol/nips/blob/8f8444d05a8842c40211ded5d10af3521541f865/42.md -- NIP-98 HTTP auth: https://github.com/nostr-protocol/nips/blob/8f8444d05a8842c40211ded5d10af3521541f865/98.md -- 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) -- Buzz implementation semantics reviewed at `bd822f3ea8fc04b449501fd4738097c32d3da950` (PR #1476) +- NIP-42 authentication: +- NIP-98 HTTP authentication: +- Companion protocol specification: [NIP-FI.md](NIP-FI.md) diff --git a/docs/nips/NIP-FI.md b/docs/nips/NIP-FI.md index 948386831c8..69eab975fe6 100644 --- a/docs/nips/NIP-FI.md +++ b/docs/nips/NIP-FI.md @@ -1,161 +1,391 @@ NIP-FI ====== -Federated Identity Authorization +Federated identity authorization -------------------------------- `draft` `optional` `relay` -**Depends on**: NIP-01 (basic event format), NIP-42 (Authentication of Clients to Relays). **Composes with**: NIP-98 (HTTP Auth), NIP-11 (Relay Information Document), NIP-OA (Owner Attestation). +**Protocol dependencies**: NIP-01, plus NIP-42 for WebSocket authorization or NIP-98 for HTTP authorization. **Optional composition**: NIP-11 discovery and a separately validated delegation protocol such as NIP-OA. ## Abstract -This NIP defines how a relay or Nostr-adjacent HTTP service authorizes an already-authenticated Nostr key only when a valid federated identity assertion resolves to the same principal and key. It specifies assertion transport, validation, an identity-to-key binding lifecycle (enroll, conflict, revoke, rotate), session semantics, and failure behavior. A separately validated delegation MAY derive narrower authority from a bound owner as described below; that exception does not turn the delegate into the federated principal. +This NIP defines how a relay or Nostr-adjacent HTTP service authorizes a Nostr key only when a valid federated identity assertion, fresh Nostr proof, current identity-to-key binding state, and the requested operation's local admission policy all agree. It defines cryptographically bound assertion transport, assertion and proof validation, read-only authorization preparation, final admission, enrollment, lifecycle state, bounded sessions, delegation, rejection behavior, discovery, and privacy. -The identity provider never becomes a Nostr signing authority, and the assertion never substitutes for Nostr proof of key control. This NIP is an authorization layer above NIP-42 and NIP-98, not a replacement for either. +The identity provider never becomes a Nostr signing authority. A bearer assertion never substitutes for Nostr proof of key control. Binding lifetime is independent of assertion lifetime: a fresh assertion can authorize an existing eligible binding after an earlier assertion expires, while every authorization lease remains bounded by the assertion used to create it. ## Motivation -Organizations deploying Nostr internally need relay access tied to their workforce identity system: an employee's relay privileges should follow their corporate identity, survive Nostr key rotation, and end at offboarding. Existing primitives each solve part of this: +Organizations may need relay access tied to an external identity system while preserving Nostr key ownership. NIP-42 proves control of a key on a relay connection, and NIP-98 proves control of a key for an HTTP request, but neither binds that key to an issuer-qualified external principal. Without a shared contract, deployments can disagree about assertion transport, key rotation, enrollment, lifecycle denial, and the point at which authorization may mutate state. -- NIP-42 proves control of a Nostr key to a connection but carries no external identity. -- NIP-05 maps an organization-controlled identifier to a pubkey, but by public DNS/HTTPS polling, not by a credential presented on the request being authorized. -- NIP-46 lets a signer demand out-of-band authentication (`auth_url`) but does not bind the resulting external subject to a key at the relay. - -Without a standard, each deployment invents an incompatible binding scheme, and the first large deployment's configuration becomes an accidental protocol. This NIP defines the contract so that any relay behind any OIDC-capable identity provider or generic OAuth2 reverse proxy (Okta, Auth0, Keycloak, oauth2-proxy, etc.) can interoperate with any conforming client. +This NIP defines a provider-neutral contract. It does not standardize an identity vendor, database schema, operator API, public identity projection, or application-specific membership policy. ## Definitions -- **assertion**: a JWT issued by a configured identity provider, presented alongside (never instead of) Nostr authentication. -- **federated identity** (`i`): the tuple `(iss, sub)` from a validated assertion. The `iss` value MUST be the exact validated issuer identifier and `sub` the exact non-empty subject string. A username, email, display name, or bare `sub` MUST NOT be used as a federated identity. -- **authorization domain** (`D`): the scope within which bindings apply, chosen by the service (an entire relay, or one tenant of a multi-tenant relay). Bindings MUST NOT cross domains implicitly. -- **binding**: an active record associating exactly one federated identity with exactly one 32-byte Nostr public key within a domain. -- **enrollment mode**: the domain's policy for creating bindings — `attested-key`, `provisioned`, or `tofu` (defined below). -- **Nostr proof**: a valid NIP-42 AUTH event (WebSocket) or NIP-98 event (HTTP) proving control of a key on the current connection or request. -- **lease**: a cached authorization decision for one `(domain, identity, key)`, bounded by the assertion's expiry. +- **assertion** (`A`): authoritative federated identity input presented as independent evidence alongside Nostr proof. Both stock profiles carry a compact-JWS JWT. A registered profile may use another private authenticated input only under its closed profile contract and normalized-result obligations. +- **federated identity** (`i`): the exact tuple `(iss, sub)` from a validated assertion. `iss` is the exact accepted issuer identifier. `sub` is the exact non-empty subject string. A username, email address, display name, employee number, mutable profile field, or bare `sub` is not a federated identity. +- **authorization domain** (`D`): a boundary selected from authenticated server routing and configuration. A client-supplied domain, forwarded host value, assertion claim, or unsigned header cannot select `D`. +- **target context** (`R_t`): the server-resolved method, authority, path and query, body digest, transport, operation, and resource for the request being admitted. +- **request context** (`R`): `R_t` sealed with the acting key returned by Nostr-proof validation. Client input cannot supply or replace that key. +- **verifier-contract fingerprint**: a versioned, implementation-supplied digest of every compiled assertion-acceptance rule that is not otherwise represented in configured policy, including protected-header handling, algorithm/key compatibility, claim normalization, and size behavior. A binary that changes any such rule MUST change this fingerprint. +- **verifier policy identity** (`policy_id`): a stable digest of assertion semantics, including the issuer or authenticated upstream-policy source, audience where applicable, allowed algorithms where applicable, authenticated key or policy-source identity, identity/key/claim mapping and normalization rules, time and size bounds, and the verifier-contract fingerprint. It MUST change when those semantics change and MUST NOT include transport, rotating key or upstream-policy contents, snapshot order, cache timestamps, or a snapshot generation. +- **normalized verified assertion result**: the closed, transport-neutral result consumed by direct authorization. It contains an issuer-qualified principal, an optional asserted key, current authorization claims or capabilities, at least one finite authority deadline, `policy_id`, the transport profile, the transport-contract revision, the profile-contract digest, and revalidation dependencies. It contains no unchecked forwarded field. +- **authorization projection**: the shared normalized identity, optional asserted key, and authorization claims or capabilities consumed by local admission. Profile identity, evidence deadlines, `policy_id`, and revalidation dependencies remain profile-specific evidence metadata. `FI-TRACE-VERIFIER-PARITY` validates that metadata for each profile, and `FI-TRACE-PREPARED-STALE` validates changed dependencies. +- **profile-contract digest**: a stable digest of the selected transport profile's authoritative fields, provenance rules, protected request components, replay semantics, and assertion-policy adapter identity. Assertion semantic changes advance `policy_id`; transport or provenance changes advance the profile-contract digest. Evidence from another digest is not interchangeable. +- **JWKS generation** (`g`): an opaque identifier for one effective verification-key snapshot. It MUST change whenever the accepted key identifiers or key material change. +- **binding**: a durable, versioned record associating one identity with one 32-byte Nostr public key in `D`. Its immutable provenance is `attested-key`, `provisioned`, or `tofu`. It MAY carry a separately authorized administrative `binding_not_after` bound. It MUST NOT derive that bound from assertion `exp` or `iat`. +- **retired pair**: a durable denial fact for one exact `(D, i, k)` pair. Ordinary authorization can never recreate that pair. +- **disabled identity**: a durable denial fact that prevents an identity from authorizing or enrolling a key. +- **revoked key**: a durable denial fact that prevents a key from authorizing or binding to any identity in `D`. +- **pending replacement**: durable lineage identifying an old key and binding version that a separately authorized recovery or re-enablement transition may consume once. +- **Nostr proof**: a valid NIP-42 AUTH event or NIP-98 event proving control of a key for the current connection or request. +- **prepared authorization**: an immutable, non-authoritative, read-only result that seals verified evidence, server-owned context, state and policy witnesses, a possible enrollment proposal, and every expiry and invalidation dependency. Producing this result creates no binding, lifecycle fact, replay claim, receipt, audit event, publication, lease, or application mutation. +- **committed authorization**: the result of revalidating a prepared authorization at final admission and atomically committing any allowed enrollment, replay claim, receipt, and required authorization audit evidence. +- **authoritative state**: state used to decide, prove, replay, or apply authorization, including bindings, lifecycle facts, replay claims, leases, capabilities, authorization receipts, required authorization audit evidence, and application state. +- **denial observation**: a capacity-bounded, non-authoritative record that a denial occurred. Its presence, absence, loss, or truncation cannot change an authorization decision or become authorization policy. +- **lease**: a cached committed decision for one actor, domain, operation set, and exact dependency versions. A lease is never a binding and cannot extend one. + +Within a domain, active bindings form a partial bijection: one identity has at most one active key, and one key has at most one active identity. ## Assertion transport -An assertion reaches the verifier in an HTTP header on the request being authorized: the WebSocket upgrade request for relay connections, or each individual request for NIP-98-authenticated HTTP endpoints. Two transport profiles are defined; a service MUST document which it accepts. +An assertion is captured on the request being authorized: the WebSocket upgrade for NIP-42 connections or the same HTTP request as its NIP-98 proof. Assertions MUST NOT appear in URLs, query parameters, Nostr events, tags, filters, application history, or public identity projections. + +Two stock transport profiles are defined: `client-attached` and `trusted-proxy-hmac-v2`. A deployment MAY also install a private registered trusted-edge profile whose identifier matches `x---v`. Server-owned listener, route, and authorization-domain configuration selects exactly one profile before accepting requests. Client input cannot select, negotiate, or downgrade it. + +This document defines transport-contract revision `2`, represented as the unsigned integer `2`. A change to the common transport-profile obligations or evidence meaning requires a new revision. Each profile contract is an immutable UTF-8 artifact whose exact bytes identify its authoritative fields, provenance rules, protected request components, replay semantics, deadlines, and assertion-policy adapter identity. Its digest is `sha256:` followed by the lowercase hexadecimal SHA-256 of those exact bytes; implementations MUST NOT apply implicit reserialization or newline normalization while computing it. A profile-specific contract change produces a new artifact and digest. + +### Transport profile obligations + +Every configured transport profile MUST: + +1. Produce one normalized verified assertion result under a closed profile contract. The result identifies an issuer-qualified principal, an optional asserted key, current authorization claims or capabilities, at least one finite authority deadline, `policy_id`, the selected profile, the transport-contract revision, the profile-contract digest, and revalidation dependencies. Deadline comparison MUST be overflow safe and equality is expired. +2. Feed the same Nostr-proof, binding, lifecycle, invalidation, lease, local-policy, and final-admission authority. An adapter cannot weaken or replace those checks. +3. Preserve the exact server-resolved domain, operation, resource, method, authority, path and query, body semantics, proof transport, and actor key used by final admission. +4. Reject absent, mixed, malformed, or profile-inconsistent evidence without falling back to or from another profile. +5. Require independent fresh Nostr proof for the current request or connection. +6. Expose every assertion and provenance deadline to lease construction, including the required finite authority deadline. +7. Keep credentials, signatures, MACs, and unredacted identity or authorization claims out of public output and observability. +8. Bind conformance evidence to one exact implementation, adapter, deployment, policy, transport-contract revision, and profile-contract digest. + +The normalized result is a security boundary, not a generic map of trusted headers. A profile contract closes its fields, types, authority, deadlines, provenance rules, and assertion-policy adapter identity. The selected assertion policy closes identity, key, claim or capability, deadline, and normalization semantics. A deployment MUST NOT treat header presence, source address, private-network location, hostname, or reachability alone as trusted-edge provenance. + +### Client-attached profile + +This profile's discovery identifier is `client-attached`. The client sends exactly one `Nostr-Federated-Identity: Bearer ` field and no assertion-provenance field. The same field is used on WebSocket upgrades and NIP-98 HTTP requests; `Authorization` remains reserved for the Nostr proof where that protocol requires it. Missing, repeated, comma-combined, malformed, empty, non-Bearer, or mixed-profile assertion fields are rejected. + +### Trusted-edge profile obligations + +Every stock or registered trusted-edge profile MUST strip inbound copies of each assertion, identity, authorization, and capability field before setting its own values. It MUST cryptographically authenticate the immediate trusted edge, deny requests that the origin cannot attribute to that edge, protect every request component used for authorization, and impose a positive finite provenance acceptance bound that expires at equality. It MUST document the accepting origin, direct-origin controls, field-stripping point, caller authentication, protected components, upstream validation, Nostr-proof path, compromise impact, and evidence location. + +A trusted-edge profile uses one of these constructions: + +- **Request-bound evidence:** a signature or MAC binds the authoritative assertion and authorization-relevant request components. A single-use replay claim also requires bounded atomic replay consumption. +- **Authenticated-edge assertion adapter:** a trusted platform validates upstream identity and policy, the immediate edge is cryptographically authenticated, the accepting origin is isolated, inbound authority-bearing fields are stripped, and the complete authorized request is integrity protected. The adapter maps only its closed validated claim set into the normalized result. + +Request-bound HMAC-v2 gives Buzz application-verified proof of the exact request and relies less on deployment-only assurances for spoof, replay, and cross-request resistance. An authenticated-edge adapter omits that application-level seal and therefore MUST demonstrate immediate-caller authentication, accepting-origin isolation, full integrity for authorization-relevant request components, inbound-field stripping, and validated upstream policy projection together. + +A registered profile is private deployment policy. Its identifier, fields, caller identity, issuer, and mechanism MUST NOT appear in NIP-11 or public examples. Registration does not weaken verifier parity or final admission. If a JWT-based registered profile permits bearer reuse, reuse of an unexpired JWT with a fresh request-appropriate Nostr proof is not itself a proxy-replay violation. The profile still rejects expired assertions, replayed or transplanted Nostr proofs, and presentation outside its configured edge; it cannot claim single-use JWT semantics unless it defines and proves them. + +### Trusted-proxy HMAC profile + +This profile's discovery identifier is `trusted-proxy-hmac-v2`. The trusted proxy strips every inbound copy of all assertion, provenance, and client-peer fields, then inserts exactly one `Nostr-Federated-Identity: Bearer ` field, exactly one `Nostr-Federated-Identity-Provenance` field, and exactly one `Nostr-Federated-Identity-Client-Peer` field. Header presence, source IP, or network topology alone is not trusted-proxy provenance. Unsigned forwarded identity MUST be rejected. + +The provenance field has this exact ASCII form: + +```text +v2... +``` + +`timestamp` is canonical unsigned decimal without leading zeroes, except that zero is `0`. `nonce` and `mac` are canonical unpadded base64url. The trusted proxy generates each nonce with at least 128 bits from a cryptographically secure random source. A decoded nonce contains at least 16 bytes, and a decoded MAC contains exactly 32 bytes. The verifier applies configured finite maximum provenance-field and nonce sizes before decoding, lookup, or replay storage. Missing, repeated, comma-combined, oversized, non-canonical, or extra components are malformed. + +The client-peer field is the proxy-authenticated end-client IP address in canonical ASCII. IPv4 uses dotted decimal without leading zeroes. IPv6 uses lowercase RFC 5952 text; an IPv4-mapped IPv6 address is encoded as canonical IPv4. The field is at most 64 bytes. Empty, repeated, comma-combined, whitespace-padded, non-IP, or non-canonical values are rejected. After verification, an implementation MAY replace the address with a domain-separated keyed digest for bounded private admission state; the raw address is not an identity claim and MUST NOT enter public output. + +The stock profile uses HMAC-SHA-256 with a deployment secret of at least 256 bits. Let `LP(x)` be the eight-byte unsigned big-endian length of byte string `x`, followed by `x`. The MAC input is: + +```text +"NIP-FI-PROXY-2" || +LP(timestamp) || LP(nonce) || LP(assertion_digest) || +LP(authorization_domain_id) || +LP(method) || LP(authority) || LP(path_and_query) || LP(body_digest) || +LP(proof_transport_code) || LP(client_peer) +``` + +For the MAC, parsed `timestamp` is encoded as an eight-byte unsigned big-endian value. `nonce`, `assertion_digest`, `body_digest`, and `mac` are their decoded bytes. `assertion_digest` is SHA-256 over the exact JWT octets after the Bearer scheme. `authorization_domain_id` is the exact 16-byte opaque identifier selected by authenticated server routing and configuration. `method` is the exact uppercase ASCII method token accepted by the endpoint. `authority` is the server-configured lowercase ASCII host, with an explicit decimal effective port and brackets around IPv6. `path_and_query` is the exact ASCII origin-form received after trusted routing: an empty path becomes `/`, the query includes its leading `?`, and percent-encoding, parameter order, and repeated parameters are preserved. It contains no fragment. A proxy rewrite is complete before these values are computed. Ambiguous or non-canonical values are rejected. `body_digest` is SHA-256 over the exact request body, including the empty body used by a WebSocket upgrade. `proof_transport_code` is exactly one byte: `0x01` for NIP-42, `0x02` for NIP-98, `0x03` for a Git smart-HTTP session, or `0x04` for Blossom. `client_peer` is the exact canonical ASCII field value. The verifier compares the MAC in constant time. + +The profile configures a positive finite `maximum_provenance_age` and a non-negative finite `future_skew`. It accepts time only when `timestamp <= now + future_skew` and `now < timestamp + maximum_provenance_age`, using overflow-safe comparisons. Equality at the age bound is expired. -1. **Trusted proxy**: an authenticating reverse proxy (for example oauth2-proxy or an SSO-aware ingress) injects the assertion after authenticating the user. The injected header name is deployment configuration. This profile is conforming only if untrusted clients cannot reach the verifier directly and the proxy strips every inbound copy of that header before setting it. This is the recommended profile for browser-based clients, which cannot attach arbitrary WebSocket upgrade headers. -2. **Client-attached**: the client sends the assertion itself in `Nostr-Federated-Identity: Bearer `. A verifier MAY additionally accept another documented header on WebSocket upgrades, including `Authorization: Bearer`; HTTP requests using NIP-98 MUST use `Nostr-Federated-Identity` because their `Authorization` header carries the `Nostr` proof. +For direct authorization through this profile, the lease deadline is no later than `min(assertion deadline, timestamp + maximum_provenance_age)`, in addition to every other applicable lease bound. -Assertion acquisition and interactive OIDC login are outside this NIP. A client-attached assertion value MUST use the `Bearer` scheme; after removing that scheme, the value MUST contain exactly one JWT and no comma-separated alternatives. +The verifier MUST reject an absent, repeated, malformed, stale, future-dated, wrong-key, or mismatched provenance value. It MUST reject v1 envelopes and any absent or invalid client-peer field. It MUST reject a committed nonce. A committed nonce is retained through at least `timestamp + maximum_provenance_age`; replay uniqueness is scoped to the authorization domain and `trusted-proxy-hmac-v2` profile and is independent of which active secret verified the MAC. An applicable Nostr-proof replay identity is retained through its entire acceptance window. The nonce and proof replay identity become consumed only during final admission. The MAC therefore cannot be replayed across an assertion, authorization domain, proof transport, client peer, method, authority, path, query, or body. Secret selection and rotation may try only a configured finite set of active secrets and fail closed when none verifies. -On a WebSocket connection, the assertion captured at upgrade is evaluated when a key performs NIP-42 AUTH — each authenticating key is authorized against that assertion independently. On HTTP, the assertion and the NIP-98 proof MUST arrive on the same request they authorize. +The proxy-to-verifier hop still requires confidentiality and integrity. Trusted listener and route configuration selects the profile in `R_t`. Direct ingress to a listener configured for this profile MUST reject assertion-bearing requests that lack valid provenance and MUST NOT fall back to `client-attached` after missing or rejected provenance. -Assertions MUST NOT be carried inside Nostr events, event tags, or subscription filters, and MUST NOT be written to relay-visible event history. +`trusted-proxy-hmac-v2` is a portable stock profile, not a mandatory deployment choice. A service that selects it MUST require valid HMAC-v2 evidence on every applicable request. Selection occurs before listeners accept protected traffic and never changes in response to request evidence. Failure MUST NOT fall back to `client-attached` or a registered profile. ## Assertion validation -The verifier is configured, per accepted issuer, with: the issuer identifier, a signing-key source (a JWKS endpoint, discoverable via OIDC `/.well-known/openid-configuration`), accepted audience values, and a claim mapping. Validation MUST enforce all of the following; any failure MUST reject the assertion: +Every adapter produces the same closed normalized verified assertion result. The two stock profiles supply compact-JWS bytes to one canonical verifier. A registered profile either uses that verifier or supplies a reviewed adapter that authenticates its closed upstream assertion and authorization claim set before producing the same result. Ordinary forwarded headers, unchecked companion fields, or adapter-local authorization cannot enter it. Claims or capabilities in the result constrain local admission; they never replace the current local policy decision. -1. The JWT signature verifies under a currently trusted key for an explicitly allowed **asymmetric** algorithm. Symmetric (HS*) and `none` algorithms MUST be rejected before any key lookup. -2. `iss` exactly equals the configured issuer identifier used to select the verification key. -3. At least one `aud` value exactly equals a configured audience. -4. `exp` is present and in the future; `nbf` and `iat`, when present, are not in the future — each within a bounded, configured clock skew. -5. The configured subject claim is present and a non-empty string. A configured claim that is absent when required, not of its expected type, or not unambiguously a single value MUST be rejected. -6. If a key claim is configured and present, it parses to exactly one 32-byte Nostr public key. Lowercase hex is the canonical encoding; `npub` bech32 MAY be accepted as a documented input normalization. +For each accepted JWT issuer, the verifier has authenticated configuration for the exact issuer identifier, accepted audiences, allowed asymmetric algorithms, key source, optional Nostr-key claim, optional authorization claim or capability mapping, finite maximum assertion age, and bounded clock skew. Policy construction incorporates the running implementation's verifier-contract fingerprint; it is not a client field or an operator-selected weakening. Transport adapters cannot change this contract. JWT validation enforces all of the following: -A display-name claim MAY be extracted as mutable metadata. It MUST NOT participate in any authorization decision. +1. The input is exactly one bounded compact JWS. Protected-header and claim member names are unambiguous. Unknown critical headers, `none`, symmetric algorithms, algorithm and key-type mismatch, and incompatible JWK `use` or `key_ops` are rejected before signature acceptance. +2. The signature verifies under exactly one currently accepted asymmetric key and explicitly allowed algorithm. A duplicate or ambiguous `kid` fails. A missing `kid` is accepted only when policy deterministically selects exactly one compatible key. +3. `iss` exactly equals the configured issuer used to select the policy and key source. +4. At least one `aud` value exactly equals an accepted audience. +5. `exp` and `iat` are finite numeric dates. The verifier requires `now < exp`, `iat <= now + skew`, and `now < iat + maximum_assertion_age`, using overflow-safe comparisons. An optional `nbf` requires `nbf <= now + skew`. Equality at an expiry or maximum-age bound is expired. +6. `sub` is a non-empty exact string within the configured size bound. +7. If a Nostr-key claim is configured and present, it resolves unambiguously to one 32-byte public key. Lowercase hexadecimal is canonical. Any additional accepted encoding must normalize to that value without ambiguity. +8. Any configured authorization claim or capability mapping accepts only its closed bounded input set and produces one deterministic normalized value. When no mapping is configured, the result contains an empty set. -Signing-key retrieval failures MUST fail closed. Verifiers SHOULD cache the key set with a bounded lifetime and SHOULD NOT refetch it in response to an unknown `kid` that was absent from a freshly fetched set, so that forged tokens cannot drive request floods to the identity provider. +The verifier bounds assertion, header, claim, subject, key-identifier, and configured key-set sizes before lookup or observability. Attacker-controlled values, including `kid`, are never emitted unsanitized. -## Nostr proof +Subject stability is an issuer trust and deployment assumption, not a property that a signed request can prove. Before accepting an issuer, the operator MUST record authoritative evidence that its selected subject is opaque, stable for the account lifetime, never reassigned, and not intentionally derived from a profile or personally identifying claim. That record identifies the issuer policy version and review owner. If the issuer can reassign a subject, the same `(iss, sub)` can name a different principal and inherit identity-scoped lifecycle or recovery authority; the issuer policy MUST remain disabled until a separately authorized remediation establishes a new non-reassignable coordinate. -The key being authorized is always the key returned by Nostr proof validation — a valid NIP-42 AUTH for the current WebSocket connection, or a valid NIP-98 event for the current HTTP request. It is never taken from an assertion claim, an unsigned request field, or client metadata. A bearer assertion alone MUST NOT authenticate a Nostr key. +The normalized verified result seals `i`, an optional asserted key `k_a`, current authorization claims or capabilities, every assertion or transport deadline, `policy_id`, the transport profile, transport-contract revision, profile-contract digest, and revalidation dependencies. For JWT profiles those dependencies include JWKS generation `g`, verification-key identity, the key snapshot's hard-validity deadline, and confidential material that can recover the exact compact-JWS bytes. Display names, email addresses, and other unchecked profile claims do not enter this result. -## Authorization +Verifier-policy identity is independent of authenticated snapshot rotation. `policy_id` MUST be derived from a deterministic, versioned encoding of every configured semantic input plus the verifier-contract fingerprint; implementations MUST publish vectors showing that every semantic change advances it while snapshot-only rotation does not. Final admission MUST deny if the current assertion-policy identity differs from the prepared identity. A changed dependency requires revalidation that reproduces the same identity, asserted key, claims or capabilities, policy identity, and live time bounds. For JWT profiles, adding, overlapping, or removing issuer keys changes `g`, not `policy_id`; evidence under `g_old` is revalidated against the current key snapshot, and an absent key, unreadable generation, or failed revalidation denies. A normal overlapping key rotation therefore does not require a new binding or policy lineage. -Given a validated assertion yielding identity `i`, optional asserted key `k_a`, and expiry `exp`, and a Nostr proof yielding key `k`, the verifier evaluates one atomic decision in domain `D`: +The base JWT verifier does not define durable JWKS anti-rollback state. If an authenticated key source republishes a previously removed key set, that document is the current snapshot and assertions under those keys may validate again. A deployment that promises rollback prevention MUST add a separately authenticated monotonic version or equivalent durable key floor and corresponding conformance evidence; otherwise key-source rollback remains residual issuer risk. + +JWT signing-key retrieval fails closed. Refresh work MUST be bounded and coalesced. An unknown `kid` cannot trigger unbounded per-request retrieval and has no stale-key fallback. A previously known key MAY be used after a soft refresh failure only under a documented finite stale-known-key policy and never after its hard maximum age. + +## Nostr proof and server-owned context + +The authorized key is always returned by Nostr proof validation, never by an assertion claim or unsigned field. + +- NIP-42 validation binds the AUTH event to the current challenge, relay URL, connection, and freshness window. +- NIP-98 validation binds the event to the exact server-resolved absolute request URL, method, payload digest when required, and freshness window. + +The service resolves `D`, operation, resource, transport, and authority from trusted server state. All evidence must agree with that same context. Unknown routes, effects, resources, domains, or transport provenance deny before preparation can become authority. + +Every protected ingress in a domain MUST use one canonical current domain policy and final-admission authority. A route with no such authority, a competing authority, or an authority at a different policy lineage makes enforcement unavailable and MUST fail closed. + +## Read-only preparation and final admission + +Authorization uses two phases. Implementations MAY combine the phases inside one transaction, but they MUST preserve the same no-mutation and revalidation properties. ```text -Authorize(D, i, k_a?, k): - if k_a exists and k_a != k: DENY (key mismatch) +PrepareAuthorization(request, assertion_input?, nostr_proof?, delegation?): + (D, R_t, operation, resource) := ResolveTargetContext(request) or DENY + + if delegation is present: + require assertion_input and all profile-specific fields are absent + ValidateNostrProof(nostr_proof, D, R_t) -> k or DENY + R := SealActor(R_t, k) + return PrepareDelegated(D, R, k, delegation) + + ValidateConfiguredTransport(D, R_t, assertion_input) -> + (verified_assertion, transport_evidence) or DENY + ValidateNostrProof(nostr_proof, D, R_t) -> k or DENY + R := SealActor(R_t, k) + (i, k_a?, claims_or_capabilities, deadlines, policy_id, + revalidation_dependencies) := + verified_assertion + if k_a exists and k_a != k: DENY(key_mismatch) + + atomically read B(i), B(k), retired(i,k), disabled(i), + revoked(k), pending(i), mode(D), and policy state + + if disabled(i): DENY(identity_disabled) + if revoked(k): DENY(key_revoked) + if retired(i,k): DENY(pair_retired) + if pending(i): DENY(explicit_replacement_required) + + if B(i) = B(k) = binding(i,k): + if binding.binding_not_after exists and + now >= binding.binding_not_after: DENY(binding_expired) + proposal := existing(binding.version, binding.provenance) + else if B(i) exists or B(k) exists: + DENY(binding_conflict) + else switch mode(D): + attested-key: + require k_a = k + proposal := enroll(i, k, attested-key) + provisioned: + DENY(binding_required) + tofu: + proposal := enroll(i, k, k_a = k ? attested-key : tofu) + + EvaluateEveryLocalAdmissionPolicy( + D, R, operation, resource, k, claims_or_capabilities + ) or DENY + return PreparedAuthorization(all evidence, proposal, witnesses, and bounds) +``` - b_i := active binding for i in D, if any - b_k := active binding for k in D, if any +An absent `binding_not_after` has no expiry. Assertion `exp`, `iat`, and maximum age never populate or extend it. Enrollment mode controls creation only; changing the mode does not rewrite or downgrade an existing eligible binding or its provenance. - if b_i = (i, k) and b_k = (i, k): ALLOW (existing binding) - if b_i exists or b_k exists: DENY (binding conflict) +Preparation is read-only, including for Attested and TOFU first use. It creates or changes no authoritative state, publication, or last-seen value. A denied preparation or local-policy decision has the same authoritative no-mutation property and MUST NOT create an authorization receipt. After the decision is fixed, the implementation attempts the non-authoritative denial observation defined under [rejection semantics](#rejection-semantics). - # no active binding on either side: enrollment - attested-key: k_a required, else DENY; create (i, k); ALLOW - provisioned: DENY (binding must be pre-created by an operator) - tofu: create (i, k); ALLOW +Final admission consumes the prepared value exactly once: + +```text +CommitAdmission(prepared, current_request): + require exact D, R, operation, resource, actor, and transport match + require every assertion, proof, transport, delegation, and policy bound is live + if prepared is DirectPrepared: + require CurrentTransportContract(D, R_t) matches the prepared profile, + transport-contract revision, and profile-contract digest + require CurrentAssertionPolicyIdentity(D, prepared.direct.i.iss) = + prepared.direct.policy_id + if a prepared revalidation dependency changed: + revalidate the configured profile's authoritative assertion input + require the normalized result, including claims or capabilities, + is equivalent + else: + require prepared is DelegatedPrepared + revalidate its delegation, relationship, owner, target, and policy witnesses + + atomically: + reread every applicable binding, lifecycle, enrollment-mode, policy, resource, + replay, and invalidation witness + unreadable state denies; changed state requires a complete recomputation + require the current result, including current claims or capabilities, + is equivalent and eligible + claim every applicable transport and proof replay identity, + including the HMAC-v2 nonce when that profile is selected + create the proposed binding only if enrollment remains eligible + append the required receipt and privacy-safe authorization audit evidence + + return CommittedAuthorization(exact actor, binding dependencies, + capabilities, dependencies, and deadline) ``` -The check and any insertion MUST be atomic for `(D, i, k)`: under concurrent first use of the same identity or key, at most one binding is created and every other attempt observes it (allow on exact match, deny on conflict). Storage failure or a race whose committed result cannot be read MUST deny — never fall back to an unchecked allow. +No committed authorization can be constructed directly from raw claims, a prepared value, cached policy, or earlier lease. A final-admission failure rolls back every authority mutation. Complete recomputation may accept only a semantically equivalent current result. If another request concurrently creates the identical eligible binding, this request may therefore recompute as `existing`; a conflicting winner denies. Storage failure or an unreadable committed result never falls back to allow. -### Enrollment modes +A denied or failed final admission MUST NOT create an authorization receipt. Its denial observation is outside the authorization transaction and cannot make a rolled-back decision authoritative. -- **`attested-key`**: the identity provider carries the user's Nostr public key in the configured key claim. First use binds only when the asserted key equals the proven key. This is the strongest mode and SHOULD be used when the identity provider can carry custom claims. -- **`provisioned`**: bindings are created only through an out-of-band administrative process; requests never create bindings. -- **`tofu`** (trust on first use): first use of an unbound identity with an unbound key creates the binding. A stolen assertion for a never-enrolled identity can bind an attacker's key in this mode; services offering it MUST document this risk. When an assertion in `tofu` mode carries a valid key claim, the binding SHOULD record the stronger `attested-key` provenance, and a binding's recorded provenance MUST NOT be downgraded by later requests. +The admitted application operation runs only after committed authorization. If the operation cannot share the authorization transaction, the implementation must use a request-bound idempotent receipt or equivalent staging so a retry cannot create a second effect from the same proof. -### Binding invariant +## Enrollment modes -Within a domain, active bindings form a partial bijection: an identity has at most one active key and a key has at most one active identity. Every state transition in this NIP preserves this invariant. +- **`attested-key`**: first use requires the assertion's key claim to equal the proven key. The created binding records `attested-key` provenance. +- **`provisioned`**: ordinary requests never create a binding. A separately authorized `ProvisionBinding` transition creates it without creating a lease; later direct use still requires a current assertion and fresh proof. +- **`tofu`**: first eligible use may create a binding without a key claim. This accepts the risk that a stolen assertion for a never-enrolled identity can bind an attacker's key. Deployments MUST label and document that risk. When a matching key claim is present, the binding records `attested-key`, not `tofu`. -## Session semantics +Binding provenance is immutable and cannot be downgraded by later requests. -For HTTP requests, the decision applies to that request only. +## Lifecycle transitions -For a NIP-42 WebSocket connection, the relay MAY cache the decision as a lease. A lease MUST NOT be honored past the assertion's `exp` (implementations MAY enforce a shorter maximum). At expiry the relay MUST reject protected operations or close the connection; a fresh assertion arrives only on a new connection's upgrade request. When a relay learns a binding was revoked, it MUST invalidate matching leases; a relay that detects revocation by polling MUST NOT claim immediate revocation and SHOULD document its detection latency. +Provisioning, retirement, disablement, revocation, rotation, recovery, re-enablement, and administrative-expiry changes are explicit privileged transitions, never side effects of ordinary authorization. Privileged authority is bound to the exact domain, operation, identity, old binding version when present, target key when present, and request. Ordinary assertion and Nostr proof cannot substitute for that authority. -When multiple keys authenticate on one connection (NIP-42 permits this), authorization is tracked per key. A lease for one key MUST NOT authorize operations attributed to another. +Every transition reads and rechecks the active relation and all applicable retired-pair, disabled-identity, revoked-key, and pending-replacement facts in one atomic transition. It appends immutable lifecycle history and triggers dependent lease invalidation after commit. Failure or stale state causes no partial mutation. -## Revocation and rotation +- **Provision binding**: allowed only in `provisioned` mode for an eligible identity and key. It creates a fresh binding version with `provisioned` provenance and no lease. +- **Retire pair**: removes the active binding, records its exact pair as retired, and records pending replacement lineage. +- **Disable identity**: records the identity as disabled. If an active binding exists, it retires that exact pair and records pending lineage. +- **Revoke key**: records the key as revoked even if it is not active. If active, it removes the binding, retires the exact pair, and records pending lineage. Repeating the same authorized revocation is idempotent and cannot erase lineage. +- **Rotate**: replaces one exact active old binding with an eligible new key, retires the old pair, and creates a fresh binding version. Rotation does not globally revoke the old key. +- **Recover**: consumes one exact pending-replacement lineage, preserves the retired old pair, and creates a fresh binding version for an eligible new key. A disabled identity uses Re-enable identity instead of Recover. +- **Re-enable identity**: requires the disabled identity, an eligible target key with fresh proof, and either no prior lineage or one exact pending lineage. It creates the new binding, clears the disabled state, and consumes present lineage exactly once. Clearing disabled state without a target would create a resurrection window: provisioned mode would have no target to match, while TOFU could let the next ordinary admission capture first use. An operator that wants to re-enable now and provision later instead leaves the identity disabled until the target and fresh proof are available. +- **Set administrative expiry**: requires one exact active binding version and sets, replaces, or clears `binding_not_after` under separate privileged policy. It advances the binding version and cannot change the pair or provenance. -Revocation is an explicit administrative or policy transition: the binding is removed from the active set and a durable revocation record is retained. A subsequent valid assertion — including one whose key claim matches the revoked key — MUST NOT reactivate a revoked binding unless the domain's documented recovery policy explicitly authorizes that transition. This prevents a replayed, still-valid assertion from silently undoing revocation. +Every new target key, including a provisioned key, requires fresh target-bound Nostr proof. When the domain requires issuer attestation for creation or replacement, the transition also requires a current assertion for the same identity with a key claim equal to the target key. Supplied stale, claimless, wrong-identity, or mismatched attestation is rejected; it cannot be treated as absent optional evidence. -Key rotation is likewise explicit, never a side effect of authorization: rotating `i` from `k_old` to `k_new` requires administrative or documented recovery authorization, an active `(i, k_old)` binding, no active binding for `k_new`, and — where the domain requires issuer attestation — a fresh assertion whose key claim equals `k_new`. The old binding is revoked and the new one created atomically, and leases for `k_old` are invalidated. A routine request presenting `i` with a new key while `(i, k_old)` is active is a binding conflict and MUST be denied. +An administrative `binding_not_after` is an authorization gate, not an implicit lifecycle transition. At or after the bound, the binding remains durable and occupies both sides of the partial bijection, but it is authorization-ineligible. Time passage alone creates no tombstone, pending lineage, or history. Restoring access requires `SetAdministrativeExpiry` or another applicable privileged lifecycle transition; ordinary authorization cannot renew the bound. ## Delegation -Delegation is outside the base primitive but composes with it. A service MAY admit a key that presents no assertion when a separately validated delegation proof (for example a NIP-OA `auth` tag) establishes an owner key that holds an active binding in the domain. The delegate key MUST NOT acquire a federated identity binding of its own through this path, and the delegate's authorization is bounded by both the owner's binding state and the delegation's own conditions. Revoking the owner's binding revokes the delegate's admission on the same schedule as the owner's own leases. +Delegation is a separate evidence path. The delegate presents fresh proof of its own key and no federated assertion. Separately validated delegation evidence seals the owner key, delegate key, relationship identifier and revision, allowed operations and conditions, exact request or target, and mandatory finite expiry. + +The service MUST resolve a current authorization-eligible owner binding and exact binding version at preparation and final admission. A cached owner lease is not substitute authority. The delegated operation is the intersection of the sealed delegation and local operation policy. The path creates or changes no owner or delegate binding, lifecycle fact, provenance, or last-seen state. + +A delegated lease requires a configured positive finite maximum. Its deadline is no later than every owner-binding, delegation, local-policy, implementation, and optional stronger owner-assertion bound. Missing finite configuration, stale owner state, actor or request mismatch, unsupported capability, unreadable dependency, or expired delegation denies. Owner retirement, disablement, key revocation, binding-version change, or relationship change invalidates dependent leases within the documented detection bound. + +## Session semantics + +HTTP authorization applies to one exact request. It does not imply a reusable lease. + +A WebSocket lease is scoped to one authenticated key, domain, operation set, direct-assertion or delegated-evidence dependencies, current binding and lifecycle versions, policy versions, and invalidation dependencies. A direct lease records its normalized result, profile and policy revalidation dependencies, and confidential material needed to revalidate the authoritative assertion input. For a JWT profile those dependencies include JWKS generation, verification-key identity, key-snapshot hard-validity deadline, and the exact compact-JWS bytes. A delegated lease instead records the exact owner binding version and relationship revision. + +The deadline is the earliest authority deadline in the normalized result and every applicable proof, transport-provenance, administrative binding, delegation, local-policy, and configured implementation bound. JWT profiles include assertion `exp`, `iat + maximum_assertion_age`, and the key-snapshot hard-validity deadline. Comparisons are overflow safe and equality is expired. + +Assertion expiry ends the lease, not the binding. Renewal requires a new connection carrying a fresh assertion on the upgrade request, followed by fresh NIP-42 proof and a complete new preparation and final admission. If the durable binding remains eligible, expiry of the assertion used for an earlier lease does not prevent the new decision. Exact assertion revalidation material is retained confidentially only through the admission or lease that may need it and is destroyed on expiry, close, or invalidation. + +Before each protected use, the service rechecks the binding and lifecycle versions, administrative bound, operation, resource, actor, lease deadline, and direct profile and assertion-policy dependencies. A changed dependency requires revalidation that reproduces the equivalent normalized result. For JWT evidence, the key snapshot must remain readable within its hard-validity deadline and a changed JWKS generation requires revalidation of the original assertion. For a delegated lease, the service rechecks the exact current owner binding and relationship revision. When another dependency changes, the service rejects protected operations or closes the connection within its documented detection bound. A polling implementation cannot claim immediate invalidation. A lease for one key never authorizes an operation attributed to another key on the same connection. ## Rejection semantics -Machine-readable rejections reuse NIP-01/NIP-42 prefixes on `OK` and `CLOSED` messages: +Implementations may retain detailed private decision reasons for audit and conformance, including `key_mismatch`, `binding_conflict`, `pair_retired`, `identity_disabled`, `key_revoked`, `explicit_replacement_required`, and `binding_expired`. Public results map them to four stable, privacy-safe classes: -- `auth-required: ` — no assertion was presented, or no NIP-42 proof has been performed. -- `restricted: ` — the assertion or proof was presented but failed validation, mismatched, conflicted with an active binding, or the identity's enrollment/binding state does not permit the operation. +| Public code | Nostr prefix | HTTP status | Meaning | +|---|---|---:|---| +| `missing_evidence` | `auth-required:` | 401 | Required assertion, proof, or delegation evidence was absent. | +| `evidence_rejected` | `restricted:` | 403 | Presented evidence or transport provenance was rejected. | +| `authorization_denied` | `restricted:` | 403 | Current binding, lifecycle, delegation, or local operation policy denied access. | +| `authorization_unavailable` | `restricted:` | 503 | Required current state could not be verified. | -HTTP endpoints respond `401` where `auth-required` applies and `403` where `restricted` applies. Rejection bodies MUST NOT echo assertion contents, claim values, or the conflicting party's identity or key. +Responses MUST NOT identify another principal or key, distinguish a conflict from a tombstone, expose issuer or claim details, echo bearer material, or reveal private policy state. An unavailable dependency never becomes an allow. + +For every denial, an implementation MUST attempt to emit an access-controlled denial observation with a stable private reason code and correlation identifier. The record contains only that code, the correlation identifier, a timestamp, a transport class, and bounded or keyed-hashed source coordinates. It MUST NOT contain raw tokens, HMAC values, or verbatim unverified claim values; failed `iss` or `sub` values are omitted, truncated, or keyed-hashed. + +The denial-observation channel is best-effort and MUST have a finite capacity bound separate from the non-reclaimable lifetime budget for required authorization audit evidence. When the channel is unavailable, full, or truncates a record, the denial still stands, authoritative state remains unchanged, and admission does not block, retry-loop, or latch on the observation write. Missing records therefore do not prove that no denial occurred. Denial observations are for monitoring and investigation only: authorization, lockout, and rate-limit policy MUST NOT derive from them. Implementations MUST monitor and alert on deny rate, dropped observations, and channel saturation. ## Discovery -A relay SHOULD advertise support in its NIP-11 document under `limitation` as `"federated_identity": true`. It MAY additionally include this top-level object: +A relay SHOULD advertise support in its NIP-11 document under `limitation` as `"federated_identity": true`. It MAY include this top-level object: ```json { "federated_identity": { - "transports": ["trusted-proxy", "client-attached"], + "transports": ["trusted-proxy-hmac-v2"], "enrollment": "attested-key", "delegation": false } } ``` -`transports` contains the supported profile names from this NIP, `enrollment` is exactly one enrollment mode, and `delegation` states whether separately validated delegation may be honored. Unknown fields MUST be ignored. A relay MUST NOT publish issuer-internal detail (tenant URLs, claim names, audiences) that is not already public. - -## Privacy +`transports` contains only the exact stock identifiers `client-attached` and `trusted-proxy-hmac-v2` for profiles implemented completely. A service MUST NOT advertise a registered profile identifier. `enrollment` is exactly one configured mode. `delegation` is true only when owner-current resolution and a positive finite delegated maximum are configured. Unknown fields are ignored. -Federated identities are typically personal data (employee identifiers). A conforming service MUST NOT publish `iss`, `sub`, assertion contents, or display-name claims in Nostr events or tags, and MUST NOT expose another user's binding state through rejection messages. Binding records, audit logs, and metrics are service-internal, and logs MUST NOT record raw bearer assertions. +A service MUST NOT enter enforcement or advertise support until every configured protected operation uses the same canonical final-admission authority, unknown protected routes fail closed, and all applicable conformance traces pass at one reviewed revision. Discovery is selected by the same server-owned domain policy as authorization. It MUST NOT expose private issuer URLs, audiences, claim names, tenant identifiers, HMAC key identifiers, or implementation-only policy detail. -## Security considerations +## Privacy -- **Issuer or proxy compromise** impersonates federated principals, but cannot satisfy Nostr proof for an already-bound uncompromised key, and in `attested-key` mode cannot bind an arbitrary key without also forging the key claim. -- **Assertion theft** cannot authorize an already-bound identity without control of the bound key. Its remaining power — enrolling a never-bound identity — exists only in `tofu` mode, which is why that mode is risk-labeled. -- **Header injection**: the trusted-proxy profile is void if clients can reach the verifier directly or the proxy forwards inbound copies of the assertion header. Deployments MUST verify both properties. -- **Algorithm confusion** is excluded by rejecting symmetric algorithms before key selection. -- **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. +NIP-FI defines no public identity projection. Protocol events, tags, filters, discovery, errors, logs, metrics, traces, and denial observations MUST NOT contain raw assertions or unredacted `iss`, `sub`, email, display name, or other private claims. Access-controlled binding, lifecycle, receipt, and authorization-audit state may retain the minimum identifiers required for enforcement and investigation. -A companion formal model of this protocol — state machine, safety and liveness properties, and attack traces — accompanies this specification. +Any separate presentation protocol is non-authoritative and cannot create, renew, prove, or revoke NIP-FI authorization. Implementations MUST bound metric and log cardinality and use redacted or pseudonymous correlation. -## Reference implementation +## Security considerations -Buzz relay: corporate identity enforcement layered above NIP-42/NIP-98/media/git/audio ingress, with JWKS validation, TOFU and attested-key enrollment, atomic binding with conflict detection, and NIP-OA delegation composition. +- **Issuer compromise** can impersonate principals but cannot prove an uncompromised already-bound Nostr key. In `attested-key` mode it must also forge the matching key claim to enroll an arbitrary key. +- **Assertion theft** cannot use an eligible existing binding without the bound key. TOFU intentionally retains first-use theft risk. +- **Proxy spoofing and replay** are limited by the configured trusted-edge profile's reviewed provenance, protected request components, deadlines, and replay semantics. HMAC-v2 supplies request-bound provenance, bounded time, one-time nonce consumption, exact assertion and body digests, and exact server-resolved routing values. An authenticated-edge adapter instead depends on the complete reviewed boundary-control set above. +- **JWKS rotation** does not change stable policy identity. Final generation revalidation prevents a key absent from the currently authenticated snapshot from authorizing. The base profile has no durable anti-rollback oracle; authenticated key-source republication of an old set is residual issuer risk. +- **Time-of-check/time-of-use races** are limited by read-only preparation and complete witness revalidation in final admission. +- **Lifecycle replay** cannot erase retired-pair, disabled-identity, revoked-key, or pending-replacement facts. Ordinary assertions never reactivate them. +- **Cross-domain and cross-request confusion** are prevented by server-owned context and exact evidence binding. +- **Availability attacks** on issuer, key retrieval, policy, binding, replay, or authorization-audit state fail closed. Attacker-reachable denials can exhaust finite observation capacity, so denial records use a separate bound, never weaken or delay the denial, and expose saturation and drop signals. +- **Delegation confusion** is limited by exact owner and delegate keys, owner binding version, relationship revision, capability intersection, target binding, and finite expiry. + +## Stable conformance labels + +The companion model and later executable matrix use these stable trace identifiers. A conforming implementation must cover every applicable trace and its boundary and concurrency subcases at one reviewed revision. Every proxy-trace and `FI-TRACE-VERIFIER-PARITY` result records `transport_contract_revision` and `profile_contract_digest`; an older revision or different profile, digest, adapter, deployment, or policy tuple cannot satisfy the claim. Each profile declares the exact spoof, replay, and cross-request artifacts its oracle exercises. The model also defines the stable safety labels `FI-INV-01` through `FI-INV-16`. + +| ID | Required property | +|---|---| +| `FI-TRACE-PROXY-SPOOF` | A trusted-edge request without the configured profile's valid provenance, including direct ingress, denies. HMAC-v2 retains its exact field, MAC, and boundary negatives. | +| `FI-TRACE-PROXY-REPLAY` | The configured trusted-edge profile enforces its declared replay semantics. For HMAC-v2, two final admissions using one proxy nonce produce at most one committed authorization and preparation consumes neither. | +| `FI-TRACE-PROXY-CROSS-REQUEST` | Changing a request component protected by the configured profile denies. HMAC-v2 protects the assertion, domain, proof transport, authenticated client peer, method, authority, path/query, and body. | +| `FI-TRACE-AUTHORITY-UNIFORM` | Every protected ingress uses the same current domain policy and final-admission authority. | +| `FI-TRACE-VERIFIER-PARITY` | Equivalent authenticated assertion input and policy produce the same authorization projection and final-admission decision at each controlled time on every transport. The same trace validates each profile's identity, revision, digest, `policy_id`, and deadlines; `FI-TRACE-PREPARED-STALE` validates changed revalidation dependencies. Deterministic vectors prove semantic changes advance `policy_id` while authenticated snapshot-only rotation does not. | +| `FI-TRACE-DOMAIN-SPOOF` | Client-selected domain or forwarded authority cannot replace server-owned context. | +| `FI-TRACE-ASSERTION-KEY-MISMATCH` | An asserted key different from the proven key denies before mutation. | +| `FI-TRACE-BINDING-CONFLICT` | A pair that conflicts with either side of the active relation denies without replacement. | +| `FI-TRACE-TOMBSTONE-REPLAY` | Fresh evidence for a retired pair, disabled identity, revoked key, or pending replacement denies ordinary authorization. | +| `FI-TRACE-ASSERTION-REFRESH` | A fresh assertion can authorize the same eligible durable binding after an earlier assertion expires. | +| `FI-TRACE-ADMIN-EXPIRY` | A fresh assertion after administrative expiry denies; only an explicit privileged transition can restore access. | +| `FI-TRACE-JWKS-ADD` | A generation change with the old key retained revalidates and may authorize the unchanged binding. | +| `FI-TRACE-JWKS-REMOVE` | A generation change that removes the signing key denies prepared evidence and leases while that key is absent from the current authenticated snapshot; an A→B→A sequence proves the deployment's declared rollback behavior. | +| `FI-TRACE-PREPARED-STALE` | Changed request or decision witnesses deny or require a complete recomputation before admission. | +| `FI-TRACE-FINAL-DENIAL-NO-MUTATION` | Denied preparation, local policy, and final admission create no authoritative mutation or authorization receipt. An available denial channel records one bounded observation; an unavailable or exhausted channel leaves the denial and authoritative stores unchanged. | +| `FI-TRACE-CONCURRENT-ENROLLMENT` | Identical eligible first uses converge on one binding version; conflicting first uses commit at most one winner. | +| `FI-TRACE-TOFU-THEFT` | Stolen-assertion first use denies except under explicit risk-labelled TOFU. | +| `FI-TRACE-DELEGATE-OWNER-ROTATED` | Owner rotation makes an old-owner delegation non-current and denies without inheritance. | +| `FI-TRACE-DELEGATION-EXPIRED` | Missing or expired finite delegation bounds deny. | +| `FI-TRACE-DENIAL-ORACLE` | Unknown, conflict, tombstone, and private-policy denials are not publicly distinguishable. | +| `FI-TRACE-DEPENDENCY-FAIL-CLOSED` | An unreadable current verifier, key, state, replay, policy, receipt, or audit dependency denies. | +| `FI-TRACE-MULTI-KEY-SESSION` | A lease for one authenticated key does not authorize another key on the same connection. | +| `FI-TRACE-CROSS-DOMAIN-COLLISION` | Equal subjects across issuers or equal pairs across domains remain distinct. | +| `FI-TRACE-PRIVACY-NONPUBLIC` | Assertion or private identity material in protocol output, public history, or observability is a conformance failure. | + +The companion [formal model](NIP-FI-MODEL.md) gives the state machine, safety and liveness properties, and the complete form of these traces.