How Buzz Implements NIP-98 HTTP Authentication: Event Signing and Verification

Buzz implements NIP-98 HTTP authentication by requiring clients to sign a Nostr event (kind 27235) containing the request URL and current timestamp, then encode it as base64 in the Authorization: Nostr <event> header for the relay to validate.

The block/buzz repository is a high-performance Nostr relay that leverages the NIP-98 specification to secure its HTTP bridge. By cryptographically binding every API request to a specific user identity and time window, Buzz ensures that REST endpoints remain protected against replay attacks and unauthorized access.

Constructing the NIP-98 Event

The authentication flow begins in the client-side authentication crate. In crates/buzz-auth/src/nip98.rs, the build_event function constructs a kind 27235 event that serves as the cryptographic proof of identity.

The function attaches two mandatory tags to the event:

  • The u tag containing the full request URL
  • The relay tag identifying the target relay

After signing the event with the caller’s private key, the implementation serializes the event to JSON and encodes it as base64 for transmission in the HTTP header【/crates/buzz-auth/src/nip98.rs#L302-L314】.

// Build a NIP‑98 event for a POST /events request
let nip98_event = buzz_auth::nip98::build_event(
    &private_key,
    &format!("https://relay.example.com/events"),
    vec![("Relay", "https://relay.example.com")],
)?;
let auth_header = format!("Nostr {}", base64::encode(serde_json::to_vec(&nip98_event)?));

// Use the header in an HTTP POST
let client = reqwest::Client::new();
let resp = client
    .post("https://relay.example.com/events")
    .header("Authorization", auth_header)
    .json(&event_body)
    .send()
    .await?;

Relay Verification and Security Controls

Once the relay receives the request, multiple validation layers ensure the authentication event is legitimate and fresh.

Timestamp Freshness Validation

The relay enforces strict temporal boundaries to prevent delayed replay attacks. In desktop/src-tauri/src/relay_admission.rs, the verification logic checks that the event’s created_at timestamp falls within a ±60 second window of the current time. The implementation also verifies that the timestamp is greater than the time the request was admitted, implementing a "wait-then-sign" pattern that prevents pre-computed request forgery【/desktop/src-tauri/src/relay_admission.rs#L366-L374】.

Host Verification

To prevent cross-site request forgery, the relay validates that the u tag in the NIP-98 event matches the host of the incoming request URL. This verification spans multiple components: the core validation logic resides in buzz-auth/src/nip98.rs, while buzz-relay/src/config.rs ensures that every operator’s configured u tag is validated against the request origin before admission【/crates/buzz-relay/src/config.rs#L212-L215】.

Replay Protection

The relay maintains a community-scoped "seen-set" of NIP-98 event IDs to prevent duplicate submissions. When a request arrives, the system checks if the event ID has already been processed for that specific community. If a duplicate is detected, the relay returns a 401 Unauthorized response with the message "NIP-98: replay detected"【/crates/buzz-relay/src/api/operator.rs#L121-L124】. This state is managed in buzz-relay/src/state.rs, which handles the persistent tracking of consumed event IDs.

Protected HTTP Endpoints

All HTTP bridge endpoints in Buzz require valid NIP-98 authentication. The router configuration in crates/buzz-relay/src/router.rs explicitly registers the following paths with the NIP-98 authentication middleware:

  • POST /events for publishing events
  • POST /query for subscription queries
  • POST /count for event counts
  • Operator admin routes for community management【/crates/buzz-relay/src/router.rs#L71-L75】

Alternative Authentication Modes

Development Mode Bypass

For local testing and development, Buzz provides a simplified authentication path. When the BUZZ_DEV_MODE environment flag is set, the relay accepts an X-Pubkey header containing the hex-encoded public key, bypassing the NIP-98 signing requirement. This shortcut is implemented in the test client suite and should never be used in production environments【/crates/buzz-test-client/tests/conformance_multitenant.rs#L965-L970】.

Git Credential Helper

The git-credential-nostr crate extends NIP-98 authentication to Git over HTTP operations. Located in crates/git-credential-nostr/src/lib.rs, this implementation signs credential request events and attaches them to the Authorization header using the same base64-encoded format as standard API requests【/crates/git-credential-nostr/src/lib.rs#L1-L8】【/crates/git-credential-nostr/src/lib.rs#L76-L78】.

// Signing a Git credential request (git-credential-nostr)
let cred_event = git_credential_nostr::sign_cred_event(
    &private_key,
    "https://relay.example.com/git/",
    &git_url,
)?;
let auth = format!("Nostr {}", base64::encode(serde_json::to_vec(&cred_event)?));

Testing NIP-98 Authentication

The test suite in crates/buzz-test-client/tests/e2e_persona.rs demonstrates the complete authentication lifecycle, including replay detection behavior. You can execute these conformance tests using Cargo:


# Using buzz‑test‑client to exercise NIP‑98

cargo test --test e2e_persona -- --nocapture

# The test creates a NIP‑98 event, posts it to /events, then verifies the

# replay detection behaviour (see the test file for details)【/crates/buzz-test-client/tests/e2e_persona.rs#L44-L53】.

Summary

  • NIP-98 events in Buzz use kind 27235 with mandatory u and relay tags, constructed in buzz-auth/src/nip98.rs.
  • Timestamp validation enforces a ±60 second window to prevent stale requests, implemented in relay_admission.rs.
  • Host verification ensures the u tag matches the request origin, preventing cross-site request forgery.
  • Replay protection maintains a community-scoped seen-set of event IDs, returning 401 errors for duplicates.
  • All HTTP bridge endpoints including /events, /query, and /count require NIP-98 headers as configured in router.rs.
  • Development mode supports X-Pubkey headers when BUZZ_DEV_MODE is enabled, while the Git credential helper extends NIP-98 to version control operations.

Frequently Asked Questions

What is NIP-98 and why does Buzz use it?

NIP-98 is a Nostr protocol extension that defines HTTP authentication using signed Nostr events. Buzz uses this mechanism to cryptographically bind HTTP API requests to specific Nostr identities, ensuring that every REST call is provably signed by the key holder and cannot be replayed across different contexts or time windows.

How does Buzz prevent replay attacks with NIP-98?

The relay stores a community-scoped set of seen NIP-98 event IDs in its state management layer. When a request arrives, Buzz checks if that specific event ID has already been used for that community. If found, the request is rejected immediately with a 401 status code and the message "NIP-98: replay detected," ensuring each authentication event can only be used once.

Which HTTP endpoints require NIP-98 authentication in Buzz?

According to the router configuration in buzz-relay/src/router.rs, all HTTP bridge endpoints require NIP-98 headers, including POST /events for publishing events, POST /query for creating subscriptions, POST /count for aggregation queries, and all operator administrative routes for community management.

Can I disable NIP-98 authentication during development?

Yes, when the BUZZ_DEV_MODE environment variable is set, the relay accepts the simpler X-Pubkey header which contains only the hex-encoded public key. This bypass allows rapid testing without cryptographic signing, though it is strictly limited to development environments and is validated in the conformance test suite.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →