# How Buzz Implements NIP-42 WebSocket Authentication: A Rust Deep Dive

> Discover how Buzz implements NIP-42 WebSocket authentication using Rust. Explore the three-stage handshake across buzz-auth, buzz-ws-client, and buzz-relay crates for secure connections.

- Repository: [Block Open Source/buzz](https://github.com/block/buzz)
- Tags: deep-dive
- Published: 2026-08-27

---

**Buzz implements NIP-42 WebSocket authentication as a three-stage cryptographic handshake involving challenge generation, signed event response, and cryptographic verification across the `buzz-auth`, `buzz-ws-client`, and `buzz-relay` crates.**

The **NIP-42 WebSocket authentication** protocol provides cryptographic identity verification for Nostr relay connections without transmitting private keys. In the **block/buzz** repository, this standard is implemented as a pure-cryptographic, stateless handshake that spans multiple Rust crates. This article examines the actual source code to show how Buzz generates challenges, constructs authentication events, and verifies signatures to secure WebSocket connections.

## The Three-Stage NIP-42 Handshake

The implementation splits responsibilities across three dedicated stages:

1. **Challenge Generation**: The relay creates a cryptographically secure random challenge.
2. **Client Response**: The client builds and signs a **kind 22242** event containing the challenge.
3. **Server Verification**: The relay validates the signature, challenge match, and timestamp before promoting the connection to an authenticated state.

### Stage 1: Challenge Generation at the Relay

In [`buzz-auth/src/nip42.rs`](https://github.com/block/buzz/blob/main/buzz-auth/src/nip42.rs), the `generate_challenge()` function produces a 64-character hex string from 32 cryptographically secure random bytes:

```rust
// buzz-auth/src/nip42.rs
pub fn generate_challenge() -> String {
    let bytes: [u8; 32] = rand::random();
    hex::encode(bytes)
}

```

When a new WebSocket connection is accepted, the relay sends this challenge as a JSON array:

```rust
// buzz-relay/src/connection.rs (excerpt)
self.send(RelayMessage::auth(&challenge));

```

### Stage 2: Client-Side Authentication Handshake

The `buzz-ws-client` crate encapsulates the entire handshake in `NostrWsConnection::connect_authenticated()`. Located in [`buzz-ws-client/src/connection.rs`](https://github.com/block/buzz/blob/main/buzz-ws-client/src/connection.rs), this method establishes the connection and initiates authentication:

```rust
// buzz-ws-client/src/connection.rs, lines 34-45
pub async fn connect_authenticated(
    url: &str,
    keys: &Keys,
    auth_tag: Option<&Tag>,
) -> Result<Self, WsClientError> {
    let mut conn = Self::connect(url).await?;
    conn.authenticate(keys, auth_tag).await?;
    Ok(conn)
}

```

The `authenticate()` method executes three critical steps:

1. **Wait for the AUTH challenge** via `wait_for_auth_challenge()`, which blocks until receiving `RelayMessage::Auth { challenge }` or times out.
2. **Build the AUTH event** using `build_auth_event()` in [`buzz-ws-client/src/message.rs`](https://github.com/block/buzz/blob/main/buzz-ws-client/src/message.rs), creating a `kind = 22242` event with `challenge` and `relay` tags.
3. **Transmit and confirm** by sending `["AUTH", <event>]` and awaiting an `OK` response with matching `event_id`.

### Stage 3: Server-Side Verification

Upon receiving an AUTH message, the relay invokes `handle_auth()` in [`buzz-relay/src/handlers/auth.rs`](https://github.com/block/buzz/blob/main/buzz-relay/src/handlers/auth.rs):

```rust
// buzz-relay/src/handlers/auth.rs, lines 38-45
pub async fn handle_auth(event: nostr::Event, conn: Arc<ConnectionState>, state: Arc<AppState>) {
    // …extract the pending challenge, verify event, then transition the connection…
}

```

The actual verification logic resides in [`buzz-auth/src/nip42.rs`](https://github.com/block/buzz/blob/main/buzz-auth/src/nip42.rs) within `verify_nip42_event()`:

```rust
// buzz-auth/src/nip42.rs, lines 44-86
pub fn verify_nip42_event(
    event: &Event,
    expected_challenge: &str,
    relay_url: &str,
) -> Result<(), AuthError> {
    // 1. Kind must be Authentication (22242)
    // 2. Schnorr signature verified via buzz_core::verify_event
    // 3. The `challenge` tag equals expected_challenge
    // 4. The `relay` tag matches normalized relay URL
    // 5. Timestamp within ±60 seconds
}

```

If verification succeeds, the connection's `AuthState` transitions from `Pending` to `Authenticated(pubkey)`. Failure results in an `OK` message with `accepted:false` and immediate connection termination.

## Integration Across the Buzz Ecosystem

Multiple Buzz applications leverage this authentication flow through the shared `buzz-ws-client` library.

The **Buzz CLI** ([`buzz-cli/src/client.rs`](https://github.com/block/buzz/blob/main/buzz-cli/src/client.rs), line 1089) calls `connect_authenticated()` before publishing events via `client.publish_event()`. Similarly, the **ACP harness** ([`buzz-acp/src/relay.rs`](https://github.com/block/buzz/blob/main/buzz-acp/src/relay.rs), line 60) automatically performs NIP-42 authentication after establishing the WebSocket connection. The **Pairing CLI** ([`buzz-pairing-cli/src/main.rs`](https://github.com/block/buzz/blob/main/buzz-pairing-cli/src/main.rs), line 412) also utilizes the same authenticated connection method to secure device pairing workflows.

## Summary

- **Challenge Generation**: `generate_challenge()` in [`buzz-auth/src/nip42.rs`](https://github.com/block/buzz/blob/main/buzz-auth/src/nip42.rs) creates 32-byte secure random hex strings for each connection.
- **Client Handshake**: `connect_authenticated()` in [`buzz-ws-client/src/connection.rs`](https://github.com/block/buzz/blob/main/buzz-ws-client/src/connection.rs) orchestrates the full authentication flow including challenge waiting and event signing.
- **Event Construction**: `build_auth_event()` creates **kind 22242** events with proper `challenge` and `relay` tags as defined in [`buzz-ws-client/src/message.rs`](https://github.com/block/buzz/blob/main/buzz-ws-client/src/message.rs).
- **Server Verification**: `verify_nip42_event()` validates Schnorr signatures, challenge strings, relay URLs, and timestamps within a ±60 second window.
- **Connection States**: Successful authentication promotes connections from `Pending` to `Authenticated(pubkey)`, enabling authorized REQ and EVENT operations.

## Frequently Asked Questions

### What is NIP-42 and why does Buzz use it?

NIP-42 is the Nostr protocol specification for WebSocket authentication that proves ownership of a private key without revealing it. Buzz uses this standard to ensure only authorized pubkeys can publish events or access restricted relay resources while maintaining full cryptographic security.

### How does Buzz generate secure authentication challenges?

Buzz generates challenges using `generate_challenge()` in [`buzz-auth/src/nip42.rs`](https://github.com/block/buzz/blob/main/buzz-auth/src/nip42.rs), which creates a 64-character hex string derived from 32 cryptographically secure random bytes using the `rand` crate. This ensures unpredictable, unique challenges for every WebSocket connection.

### What validation checks does Buzz perform on AUTH events?

According to `verify_nip42_event()` in [`buzz-auth/src/nip42.rs`](https://github.com/block/buzz/blob/main/buzz-auth/src/nip42.rs), Buzz verifies the event is **kind 22242** and validates the Schnorr signature. It also confirms the `challenge` tag matches the expected value, checks the `relay` tag against the normalized URL, and ensures the timestamp is within ±60 seconds of the current time.

### Which Buzz crates handle NIP-42 authentication?

The implementation spans three distinct crates. `buzz-auth` handles challenge generation and verification logic, `buzz-ws-client` manages the client-side handshake and event signing, and `buzz-relay` processes server-side authentication and connection state management.