Buzz 的 NIP-AA 代理认证规范:基于 NIP-OA 凭证与 NIP-42 的虚拟中继成员机制
【免费下载链接】buzzA hive mind communication platform项目地址: https://gitcode.com/GitHub_Trending/buzz14/buzz
NIP-AA(Agent Authentication,代理认证)是本仓库中一份draft/optional/relay级别的 Nostr NIP 提案,它定义了实现 NIP-43 成员制的中继(relay)应如何处理携带 NIP-OA 凭证的连接请求:只要代理(agent)的所有者(owner)是活跃成员,代理即可在 NIP-42 认证期间出示 NIP-OAauth标签获得隐式中继访问权,而无需被显式加入成员列表。阅读本文后,你将完整掌握 NIP-AA 的六步中继验证算法、虚拟成员(virtual membership)的权限边界、撤销语义与安全/隐私权衡,并能在 buzz-relay 的源码中找到对应的实现证据。
背景与动机:成员制与代理授权的割裂
NIP-43 定义了中继成员元数据,强制成员制的中继将访问限制在一个显式成员列表内;NIP-OA(Owner Attestation,见 docs/nips/NIP-OA.md)则建立了"所有者密钥授权某个代理密钥代表其行事"的密码学事实。这两个 NIP 互为补充但彼此割裂:管理员添加一位人类成员后,还必须单独为该人类运行的每一个代理逐一办理入会。
这种做法的摩擦与同步风险是显而易见的:
- 当某个成员的会员资格被撤销时,其代理仍会留在成员列表中,直到被手动清理;
- 当某位成员新建一个代理时,该代理在管理员手动添加之前无法连接。
NIP-AA 正是为了弥合这一缺口而设计:代理在 NIP-42 认证时出示 NIP-OA 凭证,中继验证凭证并检查所有者是否为活跃成员;两者均通过则放行连接。若所有者后续被撤销会员资格,代理的下一次连接尝试会自动失败——无需任何额外的清理工作。
核心术语
NIP-AA 全文遵循 RFC 2119 定义的 MUST、MUST NOT、SHOULD、SHOULD NOT、MAY 与 RECOMMENDED 语义,关键概念如下:
- owner key(所有者密钥):签发 NIP-OA 授权的 Nostr 密钥对,所有者本身是 NIP-43 意义上的中继成员;
- agent key(代理密钥):拥有自己 Nostr 密钥对的 AI 代理、机器人或自动化进程,不必是中继成员;
auth标签:NIP-OA 凭证标签["auth", "<owner-pubkey-hex>", "<conditions>", "<sig-hex>"];- NIP-42 AUTH 事件:客户端响应中继
AUTH挑战而发送的kind:22242事件; - 虚拟成员(virtual membership):从所有者成员资格派生的连接访问权,不为代理创建任何持久化成员记录;
- 活跃成员(active member):中继权威访问控制状态中将其列为未撤销、当前有效且拥有显式成员记录的公钥。通过 NIP-AA 获得访问权的虚拟成员不是活跃成员;NIP-43 的
kind:13534事件可以通告或反映这一状态,但其本身不是权威来源。
协议流程
NIP-AA 复用了标准的 NIP-42 握手,代理在构造kind:22242事件时把 NIP-OAauth标签作为普通标签嵌入其中,中继在完成 NIP-42 验证后执行 NIP-AA 附加检查。完整流程如下:
Agent Relay | | |<-- ["AUTH", "<challenge-string>"] ---| (NIP-42 step 1) | | | Build kind:22242 event: | | pubkey = agent_pubkey | | tags = [ | | ["relay", "wss://..."], | | ["challenge", "<nonce>"], | | ["auth", <owner-pubkey-hex>, | | <conditions>, | | <sig-hex>] | | ] | | Sign with agent secret key | | | |---- ["AUTH", <kind:22242 event>] -->| (NIP-42 step 2) | | | Verify NIP-42 | | Check member list | | Verify auth tag | | Check owner member| | | |<-- ["OK", "<event-id>", true, ""] --| (access granted) | | | Subsequent events MAY carry auth | | tag per NIP-OA for provenance. | | NIP-AA membership is established | | by the AUTH event; the auth tag | | on subsequent events is not | | required for relay access. |这里有一个值得强调的设计点:NIP-AA 的成员资格由 AUTH 事件本身确立,后续事件是否携带auth标签(按 NIP-OA 用于来源标注)与中继访问权无关。
失败时的响应规则:验证失败时,中继必须按下方验证算法中的错误前缀规则响应。若 AUTH 载荷损坏到无法解析出事件 id 的程度,中继必须关闭 WebSocket 连接(可先发送NOTICE消息)。这是对 NIP-42"AUTH 消息必须以 OK 应答"要求的显式例外——没有可引用的事件 id,该要求客观上无法满足。中继可以(但非必须)在任何 AUTH 失败时关闭 WebSocket;一次独立的 AUTH 失败不会隐式使连接上先前已认证的身份失效。此规则不阻止中继出于其他原因(例如所有者成员资格被撤销)刻意重新验证或终止会话。
中继验证算法:六步顺序执行
当收到 NIP-42 AUTH 事件(kind:22242)时,中继必须按下述步骤按序执行。任何一步失败都意味着 AUTH 尝试被拒绝:
- Step 1 失败(事件畸形、
id/sig无效、relay标签错误、created_at过期)→ 响应["OK", "<event-id>", false, "invalid: <reason>"]; - Step 3–5 失败(缺少凭证、凭证无效、所有者非成员)→ 响应
["OK", "<event-id>", false, "restricted: <reason>"]。
一次失败的 NIP-AA AUTH 尝试不必然使同一 WebSocket 连接上其他已认证公钥失效。
Step 1 —— 标准 NIP-42 验证
按 NIP-42 验证 AUTH 事件:event.kind必须为22242,事件id与sig必须对event.pubkey有效,relay标签必须与本中继的 URL 匹配,challenge标签必须与本连接收到的 nonce 匹配。
NIP-AA 额外要求:AUTH 事件的created_at必须落在中继定义的新鲜度窗口内,推荐 ±120 秒;窗口外的 AUTH 事件必须拒绝。任何一项检查失败即拒绝。
Step 2 —— 直接成员检查
若event.pubkey本身就是活跃成员,则按正常 NIP-43 流程放行,后续步骤不再适用。
Step 3 —— NIP-OA 凭证提取
若event.pubkey不是活跃成员,则在 AUTH 事件的标签中查找auth标签:没有auth标签则拒绝;多于一个auth标签则拒绝。
Step 4 —— NIP-OA 凭证验证
按 NIP-AA 专属流程验证auth标签。该流程复用了 NIP-OA 的密码学构造,但并不等同于完整的 NIP-OA 验证——此处不评估kind=子句(详见下文 §Kind Conditions):
- 标签必须恰好包含四个元素;
<owner-pubkey-hex>必须是合法的 64 字符小写十六进制 BIP-340 公钥;<sig-hex>必须是合法的 128 字符小写十六进制字符串;<owner-pubkey-hex>不得等于event.pubkey(禁止自认证);<conditions>必须是语法合法的 NIP-OA 条件字符串(见 NIP-OA §The Tag);- 重建预映像(preimage):
nostr:agent-auth:||event.pubkey||:||<conditions>,其中<conditions>必须原样取自auth标签——实现不得在计算预映像前对条件做重排、去重、规范化或标准化; - 计算
SHA256(preimage); - 以
<owner-pubkey-hex>为公钥,将<sig-hex>作为对该 SHA256 哈希的 BIP-340 Schnorr 签名进行验证; - 针对 AUTH 事件的
created_at字段评估created_at<t与created_at>t子句,不满足时间戳子句则拒绝。
任何一项检查失败即拒绝。
Step 5 —— 所有者成员检查
在中继成员存储中查找<owner-pubkey-hex>;若所有者不是活跃成员则拒绝。
Step 6 —— 授予虚拟成员资格
为成功 AUTH 事件中event.pubkey对应的公钥授予虚拟成员资格,不得为代理创建持久化成员记录。中继必须在连接的整个生命周期内于虚拟会话状态中保留验证通过的<owner-pubkey-hex>,以支撑按所有者维度进行的会话枚举、终止与配额聚合。
代理的访问是虚拟的,从所有者的成员资格派生而来,并且作用于特定公钥而非整个 WebSocket 连接:若连接上存在多个已认证公钥(符合 NIP-42),虚拟成员资格仅适用于完成 NIP-AA 认证的那个公钥。
若同一代理公钥在同一连接上再次完成 NIP-AA 认证(例如携带不同的auth凭证),中继必须用新凭证替换此前存储的凭证,不得将多次 AUTH 事件的凭证合并使用。
Kind Conditions:连接准入与逐事件执行的区分
凭证中的kind=子句不会在连接准入时被评估,也不影响中继是否放行。它是所有者意图的声明——说明所有者打算授权哪些事件类型——但中继的强制力在连接层面。
凭证范围警告:在 NIP-42 认证期间出示的auth标签,无论其中kind=子句如何,都会授予连接级访问权。所有者应当意识到:签发任何有效的auth标签(即使带有窄kind=条件)都会赋予代理完整的中继读写访问权,除非中继实现了可选的逐事件强制。
- 希望把代理限制到特定事件类型的所有者,必须确保中继执行逐事件
kind=限制,不应仅依赖kind=子句做访问控制; - 为事件来源标注目的(如
kind=1)签发的凭证,一旦用于 NIP-AA 就变成了中继登录凭证,这种语义扩展是有意为之。
若中继执行kind=限制,则必须在整个连接期间保留 AUTH 事件中验证过的凭证,并对event.pubkey匹配该虚拟成员公钥的每个事件,在接收、存储或转发前评估该凭证中的每一条kind=子句。逐事件强制仅针对kind=子句;created_at<与created_at>子句在连接准入(Step 4)时已评估,不会对后续事件重复评估。当逐事件强制拒绝一个EVENT时,中继必须响应["OK", "<event-id>", false, "restricted: <reason>"]。
合取语义:按 NIP-OA,单一凭证中的多个kind=子句是合取关系——事件必须满足每一条子句。条件为kind=1&kind=7的凭证不会授权任何单个事件,因为没有任何事件能同时具有两个不同的kind值。所有者应当为每个凭证使用单一kind=子句;要授权多种事件类型,要么在各自独立的连接上使用独立凭证(NIP-AA 每个 AUTH 事件只接受一个auth标签),要么使用不含kind=子句的无约束凭证。
虚拟成员权限
获得 NIP-AA 虚拟成员资格的代理可以通过中继级成员检查,包括读(订阅 REQ)与写(事件发布 EVENT)访问。但以下检查必须继续以代理自身公钥(event.pubkey)为准,除非其他规范显式定义了所有者继承:
- 频道级、群组级、配额与角色检查;
- NIP-AA不授予代理所有者的频道成员资格、群组角色或管理权限。
具体规则如下:
- 对
EVENT提交:中继必须验证event.pubkey是该连接上持有活跃或虚拟成员资格的已认证公钥;来自未认证或非成员公钥的事件必须拒绝; - 对
REQ、COUNT及其他非EVENT操作:只要连接上至少一个已认证公钥持有活跃或虚拟成员资格,中继级访问即通过;但频道级、群组级与资源级访问检查必须评估持有虚拟成员资格的那个公钥,而非所有者公钥; - 当同一连接上认证了多个公钥时,中继不得合并它们的权限,每个公钥的访问独立评估;资源级操作只有在至少一个已认证公钥独立满足该操作所需的全部中继级与资源级检查时才通过。
配额与速率限制:中继 SHOULD 在按代理公钥强制之外,额外按所有者公钥在所有源自该所有者的虚拟成员之间聚合速率限制与配额。否则,单个成员可以铸造大量代理密钥来成倍突破按公钥的配额。
管理边界:
- 虚拟成员必须不得被授予中继管理权限(具体机制实现自定,例如分配一个排除管理操作的受限角色,或在处理管理命令前检查虚拟成员状态);
- 虚拟成员必须不得被允许修改中继成员资格(添加或移除成员);
- 实现 SHOULD 在中继审计日志与成员内省 API 中把虚拟成员明确标识出来。
撤销语义
虚拟成员资格在每次新连接时检查,不会跨重连缓存。
- 所有者移除:当所有者成员资格被撤销,所有访问权源自该所有者的代理,其下一次连接尝试都会在 Step 5 失败。已活跃的会话不会被强制终止,而是持续到底层 WebSocket 连接关闭。需要立即终止会话的运维人员,应在撤销成员时主动断开活跃 WebSocket 连接;中继 SHOULD 暴露按所有者公钥枚举与终止会话的机制;
auth标签过期:若auth标签条件含created_at<t子句,中继在连接时(Step 4)以 AUTH 事件的created_at字段评估它——这约束的是 AUTH 事件自我声明的created_at字段。只有与中继强制的 AUTH 事件新鲜度(Step 1)结合,才能提供有界的授权窗口。条件评估只发生在连接准入(Step 4),除非实现显式的会话重新验证,否则中继不会在活跃会话期间重新评估条件;
注意:
created_at由代理控制。恶意代理可以把created_at设为任意值。需要硬性墙钟过期的运维人员必须独立强制。签发带短created_at<窗口并轮换的auth标签之所以能提供有界授权,正是因为 Step 1 要求 AUTH 事件的created_at落在中继新鲜度窗口内——从而阻止代理把时间回拨以绕过已过期的条件。
- 代理密钥泄露:持有有效
auth标签的代理,只要所有者仍是活跃成员且标签中created_at条件满足,即可持续重连。撤销只能通过三种方式之一实现:(a) 从成员列表移除所有者;(b)auth标签的created_at条件过期;(c) 中继应用独立的拒绝名单。NIP-OA 凭证是可复用能力——在没有上述机制之一的情况下,所有者无法单方面撤销已签发的auth标签。
安全考虑
- 重放防护:NIP-42 AUTH 事件绑定到特定中继挑战 nonce,无法跨会话重放。其中的 NIP-OA
auth标签是可复用凭证——任何持有代理私钥的人都可以构造携带同一auth标签的新 AUTH 事件。这是刻意设计:NIP-OA 凭证是能力(capability)而非一次性令牌。实现必须强制 NIP-42 挑战新鲜度;由于 NIP-AA 的重放防护完全依赖 NIP-42 的挑战质量,实现 NIP-AA 的中继 SHOULD 使用密码学上不可预测、连接唯一的挑战字符串; - 凭证范围:
auth标签不绑定特定中继或用途。连接多个中继的代理会在每个中继出示同一auth标签;为事件来源标注签发的凭证与用于 NIP-AA 中继准入同样有效。运维人员 SHOULD 在适当场合使用created_at<条件限制授权窗口; - 所有者密钥暴露:所有者公钥在 AUTH 事件的
auth标签中可见,这把所有者与代理的身份关联暴露给了处理该连接的中继(见隐私考虑); - 自认证:
<owner-pubkey-hex>等于event.pubkey的auth标签必须拒绝(Step 4),防止代理通过签署自己的凭证来引导自身访问权; - 伪造凭证:中继在 Step 4 验证 Schnorr 签名。伪造的
auth标签(签名错误)无法通过密码学验证;由非成员所有者签名的auth标签无法通过 Step 5。两种攻击都不会获得访问权; kind=过宽:由于kind=条件不在连接层强制,带kind=1条件的凭证与无约束凭证授予同样的连接级访问权。需要 kind 级限制的运维人员必须实现可选的逐事件强制(见 §Kind Conditions)。
隐私考虑
在 NIP-42 认证期间出示auth标签,会向中继披露所有者-代理关系:中继得知<owner-pubkey-hex>授权了event.pubkey(代理)。这是有意披露——中继需要该信息执行成员检查。
- 中继 SHOULD NOT 向其他成员暴露超出虚拟成员识别必要范围的所有者-代理关系;
- 不需要通过 NIP-AA 获取中继访问权的代理,可以在 AUTH 事件中省略
auth标签、改用显式成员注册,从而避免这种披露。
验证示例
以下示例使用 NIP-OA 测试密钥:
owner_secret = 0000000000000000000000000000000000000000000000000000000000000001 owner_pubkey = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 agent_secret = 0000000000000000000000000000000000000000000000000000000000000002 agent_pubkey = c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5NIP-OAauth标签(取自 NIP-OA 测试向量,条件为kind=1&created_at<1713957000):
["auth", "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", "kind=1&created_at<1713957000", "8b7df2575caf0a108374f8471722b233c53f9ff827a8b0f91861966c3b9dd5cb2e189eae9f49d72187674c2f5bd244145e10ff86c9f257ffe65a1ee5f108b369"]该标签的密码学验证(预映像、SHA256 与签名)由 NIP-OA 测试向量覆盖。以下示例描述的是各种场景下中继的预期行为;在没有完整 NIP-42 事件id与sig的情况下,它们无法独立复验。
接受场景:携带有效 NIP-OA 凭证连接的代理
条件:owner_pubkey是活跃中继成员;AUTH 事件created_at = 1713956400;中继墙钟时间接近1713956400(落在 ±120 秒新鲜度窗口内);created_at<1713957000条件满足。
- Step 1:NIP-42 验证通过,
created_at在新鲜度窗口内; - Step 2:
agent_pubkey不在成员存储中 → 继续; - Step 3:恰好找到一个
auth标签 → 继续; - Step 4:标签有四个元素;
owner_pubkey合法;owner_pubkey≠agent_pubkey;条件字符串语法合法;Schnorr 签名验证通过;created_at<1713957000被1713956400满足 → 通过; - Step 5:
owner_pubkey是活跃成员 → 通过; - Step 6:代理公钥获得虚拟成员资格。
拒绝场景一览
中继必须拒绝以下每种情况:
| 场景 | 失败步骤 |
|---|---|
auth标签签名无效(错误的 owner key) | Step 4 |
auth标签<owner-pubkey-hex>等于event.pubkey | Step 4 |
auth标签元素数少于或多于四个 | Step 4 |
auth标签<conditions>畸形(如kind=01) | Step 4 |
AUTH 事件created_at为1713957001且条件为created_at<1713957000 | Step 4 |
AUTH 事件created_at超出中继新鲜度窗口 | Step 1 |
owner_pubkey不是活跃中继成员 | Step 5 |
AUTH 事件有两个auth标签 | Step 3 |
AUTH 事件无auth标签且agent_pubkey非成员 | Step 3 |
| 虚拟成员提交中继成员管理命令(如添加/移除成员) | 虚拟成员权限(准入后) |
kind 强制示例
以下示例说明可选的逐事件kind=强制行为,所用凭证条件为kind=1&created_at<1713957000:
| 场景 | 强制启用? | 结果 |
|---|---|---|
虚拟成员发布kind:1 | 否 | 接受 |
虚拟成员发布kind:7 | 否 | 接受(仅连接级访问) |
虚拟成员发布kind:1 | 是 | 接受(满足kind=1子句) |
虚拟成员发布kind:7 | 是 | 拒绝(kind=7不在凭证中) |
与其他 NIP 的关系
- NIP-42:NIP-AA 扩展了 NIP-42 AUTH 流程,
kind:22242事件是凭证呈现载体。NIP-AA不新增任何事件类型; - NIP-OA:NIP-AA 在中继连接层消费 NIP-OA 凭证。NIP-OA 定义了
auth标签格式、签名预映像与条件语法;NIP-AA 定义了中继在 NIP-42 认证期间如何处理该标签。NIP-AA 的 Step 4 复用了 NIP-OA 的密码学构造但选择性应用:kind=子句不在连接准入时评估。这是对 NIP-OA"验证者必须评估每条子句"规则的刻意偏离——该规则适用于事件级验证,而非连接准入; - NIP-43:NIP-AA 是 NIP-43(中继访问元数据与请求)的扩展。未实现 NIP-43 的中继没有成员概念,SHOULD NOT 实现 NIP-AA;实现 NIP-43 的中继 MAY(非必须)实现 NIP-AA;
- NIP-26:NIP-OA 复用了 NIP-26 的凭证格式但未复用其语义,NIP-AA 继承了这一区分。
auth标签不得被解释为 NIP-26 委托;代理始终是其事件的唯一作者。
仓库中的工程化实现
上述规范在本仓库的 buzz-relay 与 buzz-sdk 中已有落地实现,可作为 NIP-AA 的参考实现来对照研读:
- NIP-42 AUTH 处理器(crates/buzz-relay/src/handlers/auth.rs):
handle_auth在完成纯密码学 NIP-42 验证后,依次通过社区封禁闸门、公钥白名单闸门与enforce_relay_membership成员闸门。其中extract_auth_tag_json从签名事件中提取 NIP-OAauth标签——注释明确指出该标签受事件 Schnorr 签名完整性保护,一旦被篡改,NIP-42 验证会在检查它之前失败。文件末尾的单元测试覆盖了"单个auth标签原样提取""无标签返回 None""重复标签返回 None(fail-closed)"三种情形; - 成员决策枚举与检查(crates/buzz-relay/src/api/mod.rs):
relay_members模块定义了MembershipDecision枚举(OpenRelay/Member/ViaOwner(nostr::PublicKey)/Denied),check_relay_membership在require_relay_membership开启时先查代理自身是否为成员,非成员时在allow_nip_oa_auth开启的前提下调用buzz_sdk::nip_oa::verify_auth_tag_for_auth_event验证凭证,再查所有者是否为成员并返回ViaOwner——这与 NIP-AA 的 Step 2/3/4/5 一一对应; - NIP-OA 密码学实现(crates/buzz-sdk/src/nip_oa.rs):实现
nostr:agent-auth:预映像构造、SHA256消息哈希与 BIP-340 Schnorr 签名验证,并严格校验条件字符串语法(无空白、无空子句、无前导零、kind取值范围 0–65535、created_at取值范围 0–4294967295); - 配置开关(crates/buzz-relay/src/config.rs):
BUZZ_REQUIRE_RELAY_MEMBERSHIP与BUZZ_ALLOW_NIP_OA_AUTH(均默认false)控制成员制与 NIP-OA 委托准入。前者为 true 且后者为 true 时,携带有效 NIP-OAauth标签的代理可证明其所有者是中继成员从而获得会话级访问;在开放中继上,NIP-OA 所有者提取(用于 agent→owner 反向回填)无条件进行,因为签名是密码学自证的,该开关只控制封闭中继上 NIP-OA 能否授予成员访问权。这与 NIP-AA 的"实现 NIP-43 的中继 MAY 实现 NIP-AA"的定位完全吻合。
小结
NIP-AA 用一个精炼的六步验证算法,把 NIP-43 的成员制、NIP-OA 的所有者授权凭证与 NIP-42 的认证通道缝合在一起:代理凭一张auth标签即可在所有者名下的中继获得虚拟成员资格,所有者的撤销自动传导到代理的下一次连接,且代理的权限被严格限制在连接级虚拟访问,无法继承所有者的频道、角色、配额与管理特权。对于运行 Agent 生态的中继运维者而言,理解 Step 4 与 Step 6 的语义边界、kind=条件的连接级/事件级双重语义,以及created_at自声明与中继新鲜度窗口的组合作用,是安全落地本规范的关键。
【免费下载链接】buzzA hive mind communication platform项目地址: https://gitcode.com/GitHub_Trending/buzz14/buzz
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考