block / block/buzz

NIP-OA owner attestation silently discarded on closed relays when the agent is a direct relay member

Open
#4,223 7 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
32.7k
Forks
4.3k
Avg merge
1d 13h
Merged PRs (30d)
253

Description

**Describe the bug**

On a closed relay (`require_relay_membership = true`, `allow_nip_oa_auth = true`), submitting an event with a valid `x-auth-tag` NIP-OA owner attestation does **not** materialize `users.agent_owner_pubkey` if the signing agent is already a **direct relay member**. The attestation is accepted for transport and then silently dropped.

The behavior is inverted relative to open relays: an **open** relay extracts the owner from the auth tag regardless of membership, while a **closed** relay — the stricter deployment — only materializes the owner when the agent is *not* a direct member.

**Where** (verified against `main` @ `ac4fa13`, 2026-08-01):

`crates/buzz-relay/src/api/bridge.rs:812-818`:

```rust
Ok(owner) => owner.or_else(|| {
if !state.config.require_relay_membership {
super::relay_members::extract_nip_oa_owner(&pubkey_bytes, auth_tag)
} else {
None // closed relay + direct member => owner never extracted
}
}),
```

The interaction is with `check_relay_membership` (`crates/buzz-relay/src/api/mod.rs:61`): the direct-membership fast path returns `MembershipDecision::Member` (`mod.rs:78`), which maps to `Ok(None)` — no owner. Only the `ViaOwner` branch (`mod.rs:100`, mapped at `mod.rs:131-132`) carries the owner pubkey forward. So the owner is materialized exactly when membership was granted *via* the owner, and dropped when the agent satisfied membership directly — even though the attestation is identical and valid in both cases.

**Steps to reproduce**

1. Run a relay with `require_relay_membership = true` and `allow_nip_oa_auth = true`.
2. Generate an agent keypair; compute a NIP-OA auth tag with `buzz_sdk::nip_oa::compute_auth_tag`.
3. Add the agent as a **direct relay member** (`run.sh add-member --role member`).
4. As the agent, publish a kind:0 profile over `POST /events` with the `x-auth-tag` header set.
5. Query the DB:

```sql
SELECT encode(agent_owner_pubkey, 'hex') FROM users WHERE pubkey = decode('', 'hex');
```

**Expected behavior**

`agent_owner_pubkey` is set to the attested owner. A valid attestation should be honored regardless of which membership branch granted access.

**Actual behavior**

`agent_owner_pubkey` is `NULL`. The event is accepted (`{"accepted": true}`), so there is no signal that the attestation was ignored.

Removing the direct relay membership and republishing the **identical** event materializes the owner correctly — which is what makes the cause unambiguous.

**Impact**

- `channel_add_policy: owner_only` becomes unenforceable in the intended direction: with no owner on record, the policy's owner check has nothing to match. Provisioning an agent in the natural order (admit to relay → publish profile) yields an agent whose ownership never registers.
- Owner-scoped lookups (`buzz users get --name --owner `) return nothing.
- Silent: nothing in the response or logs indicates the tag was discarded.

**Suggested fix**

Extract and materialize the owner whenever a syntactically valid auth tag is present and verifies against the signing pubkey, independent of which membership branch granted access. Concretely: either have `check_relay_membership` return the verified owner alongside `Member`, or hoist the `extract_nip_oa_owner` call out of the `require_relay_membership` conditional in `bridge.rs`.

Happy to work on a PR for this if a maintainer confirms the intended direction.

**Version and platform**

- Buzz version: self-hosted relay, `ghcr.io/block/buzz:main` @ `sha256:13ddb4c12f4c0b8147cece717a57013a45fdb1610bd072d7e383ac79a070ddd9`; behavior re-verified in source on `main` @ `ac4fa13` (2026-08-01)
- OS: Ubuntu 24.04 (relay host), NIP-11 `supported_nips [1,2,10,11,16,17,23,25,29,33,38,42,50,56,43]`

**Logs / additional context**

Related issues (searched open and closed — no duplicate found): closest adjacent are the closed-relay bootstrap cluster #2969 / #3888 (membership timing/lockout, not attestation handling). Note the interaction: the bootstrap ordering those issues push operators toward (admit first, then publish) is exactly the ordering that triggers this bug.

Contributor guide

Open the contributing guide

Research direction

Start in crates/buzz-relay/src/api/bridge.rs:812-818 and trace check_relay_membership in crates/buzz-relay/src/api/mod.rs, especially the Member and ViaOwner branches. Reproduce with a closed relay, a direct member, and a valid x-auth-tag, then verify that users.agent_owner_pubkey is populated; done means the same attestation is retained regardless of the membership branch.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
authentication, backend-api-design, security
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
70/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.