diff --git a/docs/nips/NIP-FA.md b/docs/nips/NIP-FA.md index f2554f93de..0a552d469b 100644 --- a/docs/nips/NIP-FA.md +++ b/docs/nips/NIP-FA.md @@ -132,6 +132,8 @@ Success response (`200`, `Cache-Control: no-store`): Adapters MUST issue dedicated [NIP-FI](NIP-FI.md) assertions whose protected `typ` is exactly `nip-fi+jwt` and that carry the NIP-FI required claims; as NIP-FI requires, `sub` is never an email address. The assertion's `aud` MUST be `https://` followed by the authority of the canonical `relay_url`, unchanged: `wss://community.example.com` maps to `https://community.example.com`, `wss://c.example.com:8443` to `https://c.example.com:8443`, and `wss://[2001:db8::1]:8443` to `https://[2001:db8::1]:8443`. This is the canonical host URI of the community the relay resolves from the connection's `Host`. The assertion's `nostr_pubkey` MUST be the key that signs the NIP-42 relay login; the relay rejects any other key. Clients MUST reject a response whose `nostr_pubkey` differs from the key sent in the request. +An adapter MUST NOT issue an assertion for an agent key through a user's own authentication. It MUST issue agent-key assertions only under an owner-authorized grant: a durable record, created through the owner's own authentication, that binds the agent key to the owner. A grant MAY be held by the agent as a credential or kept by the adapter for agents it operates. The adapter MUST issue only while the owner authorization recorded in the grant remains valid. Revoking the owner MUST invalidate every grant the owner authorized before the revocation, and a later authentication by the owner MUST NOT revalidate them. Agent-key assertions follow NIP-FI "Agent keys". + A client that rejects a `200` response, including one it cannot parse, MUST treat it as a refusal: keep the session, show it as refused, and not retry automatically. ### Denials @@ -180,6 +182,5 @@ NIP-FI defines no public identity projection. Adapter responses are private logi ## Out of scope -- Assertions for agent keys. This NIP covers assertions for keys a user authenticates with directly; issuing assertions for agent keys requires a separate design. - Discovery of which relays require enterprise identity and which adapter serves them. - The upstream identity protocol between the adapter and its identity provider. diff --git a/docs/nips/NIP-FI.md b/docs/nips/NIP-FI.md index 9448d6bc66..5329404b75 100644 --- a/docs/nips/NIP-FI.md +++ b/docs/nips/NIP-FI.md @@ -127,6 +127,30 @@ authority. Signature verification remains mandatory before acceptance. `VerifyAssertion` MUST compare `aud` with the resolved community's expected audience; issuer policy selection by `iss` MUST NOT broaden either binding. +### Agent keys + +An issuer MAY issue an assertion whose `nostr_pubkey` names an agent key +acting for an owner key. Such an assertion MUST carry `nostr_owner_pubkey`: +the lowercase hexadecimal encoding of one 32-byte public key, distinct from +`nostr_pubkey`. An issuer MUST NOT issue an assertion without +`nostr_owner_pubkey` for a key it has issued an agent assertion for, and MUST +NOT issue an agent assertion while the owner is not authorized by the issuer. + +A relay advertising `agent_assertions: true` MUST verify `nostr_owner_pubkey` +as part of the assertion. A malformed value, or one equal to `nostr_pubkey`, +MUST deny `evidence_rejected`. The relay MUST record the value as the +owner of the session or request the assertion admits. If the relay already +associates a different owner with the asserted key in the resolved community, +it MUST deny `authorization_denied`. +For every agent assertion, regardless of whether the asserted key is itself +authorized in the community, the relay MUST deny when the community bans the +asserted key or the owner, or when the issuer has denied either key. Only +then, when the asserted key is not itself authorized in the community, the +relay MUST authorize it if and only if the issuer is trusted for the community +and the owner is authorized in the community when access is decided. A relay +not advertising `agent_assertions: true` MUST deny an assertion carrying +`nostr_owner_pubkey` `evidence_rejected`. + ### Unverified claims Relays MUST ignore claims they do not verify. An unverified claim — including @@ -248,8 +272,14 @@ VerifyAssertion(token, community, D, R_t): AssertAudienceMatch(claims.aud, community.expected_aud) or DENY(evidence_rejected) AssertTimeBounds(claims, policy) or DENY(evidence_rejected) // [FI-TRACE-ASSERTION-VALIDATION] k_claimed := ParseHexKey(claims.nostr_pubkey) or DENY(evidence_rejected) + owner := absent + if claims.nostr_owner_pubkey is present: + AssertAgentAssertionsAdvertised(relay) or DENY(evidence_rejected) + owner := ParseHexKey(claims.nostr_owner_pubkey) or DENY(evidence_rejected) + AssertNotEqual(owner, k_claimed) or DENY(evidence_rejected) return VerifiedAssertion(identity=(claims.iss, claims.sub), asserted_key=k_claimed, + owner=owner, authority_deadlines=ComputeDeadlines(claims, snapshot)) ``` @@ -275,15 +305,19 @@ On WebSocket upgrade: [FI-TRACE-ASSERTION-KEY-MISMATCH] 6. Register the session's proven `k` and admitting issuer `iss` in the relay's session table, making it visible to the disconnect close scan. + Registration includes the session's owner, if any. Registration MUST occur before the deny-set check in step 7. This ordering ensures any connection that straddles a concurrent disconnect is caught by one side or the other: either the close scan sees the registered session, or the deny-set check (step 7) sees the inserted entry. Both checks use the - same `(iss, k)` key. A concurrent disconnect close scan MAY cancel this - registered connection before step 8; cancellation is terminal, so a + same keys: `(iss, k)` and, for a session with an owner, + `(iss, verified.owner)`. A concurrent disconnect close scan MAY cancel + this registered connection before step 8; cancellation is terminal, so a cancelled connection MUST NOT be admitted. -7. Check deny set for `(iss, k)`; active entry (`now < until`) → deny - `authorization_denied`. [FI-TRACE-DENY-SET] +7. Check the deny set for `(iss, k)` and, if `verified.owner` is present, for + `(iss, verified.owner)`. Either active entry (`now < until`) denies + `authorization_denied`. + [FI-TRACE-DENY-SET] 8. Admit the connection only if it remains live and uncancelled. The session's authority deadline is the minimum of all `authority_deadlines`; see Session policy. @@ -351,9 +385,10 @@ A disconnect call causes the relay to: capacity and a new entry cannot be inserted, the relay MUST reject the command `503`; no sessions are closed and no replay state is consumed. 2. Close all live WebSocket connections registered under the command's issuer - whose proven `k` equals the target pubkey, whether or not admission has - completed, synchronously. Disconnect cancellation is terminal: a cancelled - in-progress connection MUST NOT be subsequently admitted. Connections + whose proven `k` equals the target pubkey or whose recorded owner equals + the target pubkey, whether or not admission has completed, synchronously. + Disconnect cancellation is terminal: a cancelled in-progress connection + MUST NOT be subsequently admitted. Connections registered under another issuer are not affected. The deny set is held **in relay memory only** — no durable storage, no schema @@ -517,12 +552,14 @@ performs all pure authorization checks and then, as its single atomic admission mutation (step 7), simultaneously reserves the `(iss, jti)` replay identity and inserts the deny entry — both or neither. A capacity failure at that step rejects `503`; neither the jti nor the deny entry is recorded, and the caller may safely -retry the same signed command. On success, the relay closes all live connections -registered under the command's issuer (`claims.iss`) whose proven `k` equals +retry the same signed command. On success, the relay MUST close every live connection +registered under the command's issuer (`claims.iss`) whose proven key equals +`CommandResult.target_pubkey` or whose recorded owner equals `CommandResult.target_pubkey`, whether or not admission has completed. Disconnect cancellation is terminal: a cancelled in-progress connection MUST NOT -be subsequently admitted. This close scan matches the same -`(iss, target_pubkey)` key as the deny entry; connections registered under +be subsequently admitted. This close scan matches the deny +entry's `target_pubkey` against each connection's proven key and recorded +owner; connections registered under another issuer are not affected. The `until` expiry is taken exclusively from the signed command JWT claim; the request body carries no `until` field. An unknown or unprovable pubkey is not an error; the relay responds `200` @@ -743,8 +780,10 @@ On each protected HTTP request: `evidence_rejected`. [FI-TRACE-HTTP-INGRESS] 5. Assert `verified.asserted_key == k`; mismatch → deny `authorization_denied`. [FI-TRACE-ASSERTION-KEY-MISMATCH] -6. Check deny set for `(iss, k)`; active entry (`now < until`) → deny - `authorization_denied`. [FI-TRACE-DENY-SET] +6. Check the deny set for `(iss, k)` and, if `verified.owner` is present, for + `(iss, verified.owner)`. Either active entry (`now < until`) denies + `authorization_denied`. + [FI-TRACE-DENY-SET] 7. Admit the request. A body is **authorization-relevant** whenever any body byte influences the @@ -809,14 +848,16 @@ normative behavior for them: - Per-session (`sid`-level) revocation: issuer-side. The relay's revocation primitive is pubkey-scoped and community-wide, keyed `(iss, target_pubkey)` (see Admin disconnect API). If an issuer revokes an entire identity, it - enumerates and disconnects each key it has asserted for that identity. + disconnects each key it has asserted for that identity. A disconnect of an + owner key also covers that owner's agent keys at relays advertising + `agent_assertions: true`. Individual session/device logout is handled by assertion expiry within the configured TTL window. - Revocation signaling to the issuer/IdP: issuer-side; the issuer stops issuing assertions, which closes the relay window within assertion TTL. - Directory integration and account-offboarding automation: issuer-side. - Audit logging beyond what the relay operator chooses to retain: issuer-side. -- Delegation: out of scope. +- Delegation other than "Agent keys": out of scope. ## Discovery @@ -848,6 +889,11 @@ issuer stops issuance after a successful deny, the residual ceiling is issuer-operational property, not a protocol invariant this field can advertise unconditionally. +`agent_assertions` (boolean, optional). `true` means the relay admits agent +assertions as defined in "Agent keys". Absent or `false` means it does not. +An issuer MUST NOT issue an agent assertion for a community whose relay does +not advertise `agent_assertions: true`. + Discovery MUST NOT state issuer URLs, audiences, claim names, tenant IDs, or deployment-local identifiers. [FI-TRACE-DISCOVERY-PRIVATE] @@ -896,7 +942,8 @@ For the deny-until-TTL disconnect model (issuer issues a successful disconnect call with an `until` timestamp), the relay inserts a deny entry for the target pubkey with expiry `until` and then closes all matching connections synchronously. Here, matching means connections registered under that issuer whose proven `k` -equals that target pubkey, whether or not admission has completed; disconnect +equals that target pubkey or whose recorded owner equals the target pubkey, +whether or not admission has completed; disconnect cancellation is terminal, so a cancelled in-progress connection MUST NOT be subsequently admitted. Connections registered under another issuer are not affected. Any subsequent admission attempt for that pubkey under the same issuer