<>ts-stack
Get StartedArchitecturePackagesSpecsGuides
⌘K
Reference
Home
Get StartedOverviewInstallChoose your stackKey concepts
ArchitectureOverviewStack layersBEEF (BRC-62)BRC-100 Wallet InterfaceIdentity & AuthConformance pipeline
PackagesOverviewSDKWalletNetworkOverlaysMessagingMiddlewareHelpers
InfrastructureOverviewmessage-box-serveroverlay-serveruhrp-server-basicuhrp-server-cloud-bucketwabwallet-infrachaintracks-server
SpecsOverviewBRC-100 Wallet InterfaceOverlay HTTPMessage-box HTTPAuthsocket (WebSocket)BRC-31 Auth HandshakeBRC-29 Peer PaymentBRC-121 / HTTP 402ARC BroadcastMerkle ServiceStorage AdapterGASP SyncUHRPAir-Gap Optical (BRC-141)
ConformanceOverviewVector catalogTS runnerContributing vectors
GuidesOverviewBuild a wallet-aware appRun an overlay nodePeer-to-peer messagingHTTP 402 payments
ReferenceOverviewBRC indexRepository health
AboutVersioningContributingDoc agentDocumentation sources
Loading…
Edit this page on GitHub
© 2026 BSV Blockchain. ts-stack is open-source.
GitHubContributingVersioning

BRC-31 Mutual Authentication Handshake

BRC-31 (implemented via BRC-103 Peer + BRC-104 HTTP transport) enables cryptographic handshakes between client and server. Both parties prove control of identity keys using signatures, and authenticated application messages are signed and verified. No shared password is required.

Interactive spec

At a glance

FieldValue
FormatAsyncAPI 3.0
Version1.0.0
Statusstable
Implementations@bsv/auth-express-middleware, @bsv/authsocket

What problem this solves

Verifiable peer identity without a conventional account token. BRC-31 uses signatures to prove control of an identity private key. Certificate trust and application authorization remain separate concerns. ECDSA is not post-quantum cryptography.

Replay resistance with session state. Session nonces and a fresh 32-byte request ID are bound into signed request/response payloads. The SDK session manager must preserve and validate that state; a nonce string alone is not an expiring single-use store. The signed HTTP payload does not introduce a separate timestamp field.

Certificate exchange. The handshake optionally requests verifiable certificates (e.g., age verification, credential proofs) that the client provides. The server can validate these against known certifiers, enabling selective disclosure (e.g., "prove you're over 18" without revealing your actual birthdate).

Protocol overview

Two-phase handshake (followed by authenticated requests):

Phase 1 — Initial Exchange (non-general)

  1. Client → Server POST /.well-known/auth with initialRequest message

    • Client's public key in x-bsv-auth-identity-key header
    • Client's nonce in x-bsv-auth-nonce header
    • Client signature over the nonce in x-bsv-auth-signature header
  2. Server → Server (validates nonce, generates its own nonce)

    • Returns 200 with initialResponse message
    • Server's public key in x-bsv-auth-identity-key response header
    • Server's nonce in x-bsv-auth-nonce response header
    • Server signature (covering client's nonce and server's nonce) in x-bsv-auth-signature header
    • If requesting certificates: x-bsv-auth-requested-certificates header with certificate types
  3. Client → Server (if server requested certificates)

    • POST /.well-known/auth with certificate payload
    • Server waits up to 30 seconds; if timeout returns 408

Phase 2 — General Authenticated Requests

After handshake succeeds, every request/response carries:

  • Request headers:

    • x-bsv-auth-version — protocol version
    • x-bsv-auth-identity-key — client's public key
    • x-bsv-auth-nonce — server's last nonce
    • x-bsv-auth-your-nonce — client's last nonce
    • x-bsv-auth-request-id — fresh random 32-byte value
    • x-bsv-auth-signature — ECDSA signature over requestId || method || path || headers || body
  • Response headers (same pattern, server signs):

    • x-bsv-auth-identity-key, x-bsv-auth-nonce, x-bsv-auth-your-nonce, x-bsv-auth-request-id, x-bsv-auth-signature
    • Signature covers requestId || statusCode || headers || body

Key types / endpoints

ChannelDirectionMessage TypePurpose
/.well-known/authRequestinitialRequestClient initiates handshake with nonce
/.well-known/authResponseinitialResponseServer validates, responds with nonce + signature
/.well-known/authRequestcertificatePayloadClient provides certificates if server requested
Any routeRequestgeneralAuthenticated application request (after handshake)
Any routeResponsegeneralAuthenticated application response

Example: Express middleware handshake

typescript
import express from 'express'
import { createAuthMiddleware } from '@bsv/auth-express-middleware'
import { PrivateKey, ProtoWallet } from '@bsv/sdk'

// 1. Create wallet for signing/verifying
const wallet = new ProtoWallet(PrivateKey.fromHex(process.env.SERVER_PRIVATE_KEY!))

// 2. Create auth middleware
const authMiddleware = createAuthMiddleware({
  wallet,
  allowUnauthenticated: false // Reject unauthenticated requests with 401
})

const app = express()
app.use(express.json())
app.use(authMiddleware) // Install middleware early

// 3. Routes now have req.auth.identityKey set to client's public key
app.get('/protected', (req, res) => {
  res.json({
    message: `Hello, ${req.auth.identityKey}`,
    authenticated: true
  })
})

app.listen(3000)

Client-side (using AuthFetch from @bsv/sdk):

typescript
import { AuthFetch } from '@bsv/sdk'

const authFetch = new AuthFetch(walletClient)

// Handshake + signature happen transparently
const response = await authFetch.fetch('https://server.com/protected', {
  method: 'GET'
})

const data = await response.json()
console.log(data.message) // "Hello, 025706528f0f6894b2ba505007267ccff1133e004452a1f6b72ac716f246216366"

Example: WebSocket with AuthSocket

typescript
import http from 'http'
import { AuthSocketServer } from '@bsv/authsocket'

const server = http.createServer()

// 1. Wrap with BRC-103 authentication
const io = new AuthSocketServer(server, {
  wallet: serverWallet,
  cors: { origin: '*' }
})

// 2. Listen for authenticated connections
io.on('connection', socket => {
  console.log('Authenticated:', socket.id)

  // All messages from this socket are auto-verified
  socket.on('chatMessage', msg => {
    console.log('Message verified:', msg)
  })
})

server.listen(3000)

Client connects with BRC-103 handshake; all WebSocket messages are signed/verified.

Conformance vectors

BRC-31-related portable coverage currently lives in conformance/vectors/messaging/brc31/authrite-signature.json:

  • Nonce generation and freshness
  • Signature verification with ECDSA
  • Replay attack prevention (nonce reuse detection)
  • Request ID binding to headers and body
  • Certificate request/response flow
  • Session establishment and timeout handling

Implementations in ts-stack

PackageNotes
@bsv/auth-express-middlewareExpress.js middleware for HTTP BRC-31 authentication; intercepts response methods to sign responses
@bsv/authsocketSocket.IO wrapper adding BRC-31 authentication to WebSocket connections
@bsv/sdkPeer and Transport abstractions; AuthFetch client implementation

Related specs

  • BRC-100 Wallet — Wallet interface (provides identity keys and signing)
  • BRC-103 Peer Auth — Underlying peer-to-peer mutual auth primitives
  • BRC-104 HTTP Transport — HTTP header protocol for BRC-103
  • BRC-121 / 402 — Often stacked after BRC-31 for monetized endpoints

Spec artifact

brc103-mutual-auth.yaml