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
utag containing the full request URL - The
relaytag 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 /eventsfor publishing eventsPOST /queryfor subscription queriesPOST /countfor 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
uandrelaytags, constructed inbuzz-auth/src/nip98.rs. - Timestamp validation enforces a ±60 second window to prevent stale requests, implemented in
relay_admission.rs. - Host verification ensures the
utag 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/countrequire NIP-98 headers as configured inrouter.rs. - Development mode supports
X-Pubkeyheaders whenBUZZ_DEV_MODEis 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →