Raw YAML source
asyncapi: '3.1.0'
info:
title: BRC-103 Mutual Authentication Handshake
version: '1.0.0'
description: |
AsyncAPI 3.0 specification for the BRC-103 peer-to-peer mutual
authentication protocol (https://bsv.brc.dev/peer-to-peer/0103) as
implemented in:
- `packages/middleware/auth-express-middleware/src/index.ts`
(`ExpressTransport`, `createAuthMiddleware`) — BRC-104 HTTP transport
(https://bsv.brc.dev/peer-to-peer/0104)
- `packages/messaging/authsocket/src/SocketServerTransport.ts`
(`SocketServerTransport`) — Socket.IO transport
- `@bsv/sdk` `Peer` and `Transport` interfaces
This file replaces the incorrectly named `brc31-handshake.yaml`.
BRC-31 Authrite is a separate protocol, not an alias for BRC-103.
BRC-104 covers the HTTP binding.
## Protocol overview
BRC-103 uses derived signing keys and nonce-bound signatures for peer
authentication. It does not by itself provide forward-secret transport
encryption; applications must choose their transport and authorization policy.
### Two-phase handshake
**Phase 1 — Non-general (initial exchange)**
Carried over the special endpoint `POST /.well-known/auth` for HTTP
transports, or the `authMessage` Socket.IO event for WebSocket transports.
1. **Client → Server** `initialRequest`
The SDK emits exactly `version`, `messageType`, `identityKey`,
`initialNonce`, and `requestedCertificates`. The initial request has no
`nonce`, `payload`, or `signature` member. The HTTP transport posts this
JSON with `Content-Type: application/json`; general-message auth headers
are not added to this handshake request.
2. **Server → Client** `initialResponse`
The server creates its own `initialNonce`, echoes the client's nonce as
`yourNonce`, and signs the pair. It returns a JSON AuthMessage containing
`version`, `messageType`, `identityKey`, `initialNonce`, `yourNonce`,
`signature`, `requestedCertificates`, and optional `certificates`.
The client validates the response before treating the peer as authenticated.
**Phase 2 — General (authenticated request/response)**
Once the session is established all subsequent HTTP requests carry:
```
x-bsv-auth-version: <version>
x-bsv-auth-identity-key: <clientPubKeyHex>
x-bsv-auth-nonce: <base64Nonce>
x-bsv-auth-your-nonce: <base64ServerNonce>
x-bsv-auth-request-id: <base64RequestId>
x-bsv-auth-signature: <hexDERSignature>
```
The signed payload includes: `requestId || method || pathname || search
|| headers (sorted) || body`.
The server responds with:
```
x-bsv-auth-version: <version>
x-bsv-auth-identity-key: <serverPubKeyHex>
x-bsv-auth-nonce: <base64Nonce>
x-bsv-auth-your-nonce: <base64ClientNonce>
x-bsv-auth-request-id: <base64RequestId>
x-bsv-auth-signature: <hexDERSignature>
```
The signed response payload includes: `requestId || statusCode ||
headers (sorted) || body`.
### Certificate flow (optional)
If the server declares `certificatesToRequest`, it embeds the request set
in the `requestedCertificates` field of the `initialResponse` JSON body.
The client then provides certificates in a follow-up `/.well-known/auth`
call before the `next()` middleware proceeds. The server waits up to
30 seconds; timeout returns 408.
In v0.1 the initial request is unsigned, and the initial-response signature
covers the nonce pair rather than the optional `requestedCertificates` or
`certificates` fields. Built-in proving encrypts revelation keys to the
destination identity, but applications must not disclose plaintext or
authorize side effects from an initial certificate-request callback.
Authenticate policy-sensitive requests after the handshake.
`RequestedCertificateSet` is a legacy allowlist without all-of, any-of,
threshold, or optional-field semantics. Validation does not assert that
every listed certificate type or field was supplied. Relying applications
must inspect the actual validated certificates and decrypted fields before
granting access.
### Unauthenticated pass-through
If `allowUnauthenticated: true` is set in middleware options, requests
without auth headers proceed with `req.auth.identityKey = 'unknown'`.
## Implementation-specific notes
- `ExpressTransport` intercepts `res.status`, `res.json`, `res.send`,
`res.set`, `res.end`, `res.sendFile`, and `res.text` to buffer the
response until after `Peer.toPeer` signs and re-emits it. Original
methods are saved as `res.__status`, `res.__json`, etc.
- The `RequestId` is a 32-byte random value encoded as base64.
- A wallet-authenticated nonce is not independently a single-use or expiring
token. Preserve the protocol's signature and session checks and apply
application-level idempotency to protected operations.
- Shared `AsyncSessionManager` stores must retain the complete PeerSession,
including local `certificatePolicy` and `pendingCertificateRequests`, and
coordinate concurrent writers. These fields are never part of AuthMessage.
- Certificate responses are checked against locally recorded policies, never
against a policy supplied in the response. Dynamic requests are scoped to
their session. v0.1 responses do not echo a request ID, so a response must
match one complete outstanding local set or the local handshake set.
- Certificate listeners are observers: validation is committed and waiters
are released first. A listener error cannot revoke or veto that validation.
servers:
httpServer:
host: '{host}'
pathname: '/.well-known/auth'
protocol: https
description: |
HTTP endpoint for BRC-103 non-general (initial handshake) messages.
General (authenticated) messages use normal application paths.
variables:
host:
default: messagebox.babbage.systems
# ---------------------------------------------------------------------------
# Components
# ---------------------------------------------------------------------------
components:
schemas:
PubKeyHex:
type: string
pattern: '^0[23][0-9a-fA-F]{64}$'
description: Compressed secp256k1 public key, 66 hex characters.
Base64String:
type: string
description: Base64-encoded binary data.
HexString:
type: string
pattern: '^[0-9a-fA-F]+$'
description: Hex-encoded binary data.
AuthMessageType:
type: string
enum: [initialRequest, initialResponse, certificateRequest, certificateResponse, general]
description: |
- `initialRequest` — first message from the initiating party
- `initialResponse` — response from the receiving party completing Phase 1
- `general` — signed application message (Phase 2)
AuthMessage:
type: object
description: |
The core BRC-103 message envelope. Transported over HTTP bodies
(at `/.well-known/auth`) or Socket.IO `authMessage` events.
required: [messageType, version, identityKey]
properties:
messageType:
$ref: '#/components/schemas/AuthMessageType'
version:
type: string
description: Auth protocol version (e.g. "0.1").
identityKey:
$ref: '#/components/schemas/PubKeyHex'
description: Public identity key of the sender of this message.
nonce:
$ref: '#/components/schemas/Base64String'
description: |
Fresh single-use random value generated by the sender.
Stored in the `SessionManager` to prevent replay. Multi-instance
deployments should store it in a shared `AsyncSessionManager`
implementation. The SDK's in-process signed-message claims fail
closed at a per-session bound. Its unsigned initial-request cache
uses a bounded time/cardinality replay window and evicts the oldest
claim at capacity so unauthenticated traffic cannot globally stop
new handshakes.
yourNonce:
$ref: '#/components/schemas/Base64String'
description: Echo of the peer's nonce from the previous message.
initialNonce:
$ref: '#/components/schemas/Base64String'
description: |
The sender's session nonce in initialRequest, initialResponse,
certificateRequest and certificateResponse. It is not a per-request
correlation identifier.
payload:
type: array
items:
type: integer
minimum: 0
maximum: 255
description: |
For `general` messages: the signed application payload.
Encoding: `requestId(32) || VarInt(statusCode) || VarInt(nHeaders)
|| [header pairs] || VarInt(bodyLength) || body` (bodyLength is -1
with no body bytes when the body is empty).
Absent from SDK initialRequest and initialResponse messages.
signature:
type: array
items:
type: integer
minimum: 0
maximum: 255
description: Message-specific DER signature; absent from initialRequest. InitialResponse signs the nonce pair, while general and certificate messages sign their respective payloads.
requestedCertificates:
type: object
description: |
Locally chosen certificate request set in initialRequest,
initialResponse and certificateRequest. CertificateResponse does not
echo this field; receivers validate against their local policy.
additionalProperties: true
# -------------------------------------------------------------------------
# HTTP header shapes (informational — not AsyncAPI schema objects)
# -------------------------------------------------------------------------
InitialRequestHeaders:
type: object
description: The SDK HTTP transport sends handshake data in the JSON body.
required: [Content-Type]
properties:
Content-Type:
type: string
enum: [application/json]
InitialResponseHeaders:
type: object
description: Handshake fields are in the JSON response body. General-message auth headers are not required.
properties:
Content-Type:
type: string
GeneralRequestHeaders:
type: object
description: |
HTTP request headers sent by the client for every authenticated
application request (Phase 2).
required:
- x-bsv-auth-version
- x-bsv-auth-identity-key
- x-bsv-auth-nonce
- x-bsv-auth-your-nonce
- x-bsv-auth-request-id
- x-bsv-auth-signature
properties:
x-bsv-auth-version:
type: string
x-bsv-auth-identity-key:
$ref: '#/components/schemas/PubKeyHex'
x-bsv-auth-nonce:
$ref: '#/components/schemas/Base64String'
x-bsv-auth-your-nonce:
$ref: '#/components/schemas/Base64String'
x-bsv-auth-request-id:
$ref: '#/components/schemas/Base64String'
description: 32-byte random value, base64-encoded.
x-bsv-auth-signature:
$ref: '#/components/schemas/HexString'
description: |
ECDSA signature over:
`requestId(32B) || VarInt(method.length) || method
|| VarInt(pathname.length) || pathname
|| VarInt(search.length) || search (or -1 if empty)
|| VarInt(nHeaders) || [sorted header pairs]
|| VarInt(bodyLength) || body (or -1 if empty)`.
GeneralResponseHeaders:
type: object
description: |
HTTP response headers set by the server on every authenticated
application response (Phase 2).
required:
- x-bsv-auth-version
- x-bsv-auth-identity-key
- x-bsv-auth-nonce
- x-bsv-auth-your-nonce
- x-bsv-auth-request-id
- x-bsv-auth-signature
properties:
x-bsv-auth-version:
type: string
x-bsv-auth-identity-key:
$ref: '#/components/schemas/PubKeyHex'
x-bsv-auth-nonce:
$ref: '#/components/schemas/Base64String'
x-bsv-auth-your-nonce:
$ref: '#/components/schemas/Base64String'
x-bsv-auth-request-id:
$ref: '#/components/schemas/Base64String'
x-bsv-auth-signature:
$ref: '#/components/schemas/HexString'
description: |
ECDSA signature over:
`requestId(32B) || VarInt(statusCode)
|| VarInt(nHeaders) || [sorted non-auth x-bsv header pairs]
|| VarInt(bodyLength) || body (or -1 if empty)`.
# -------------------------------------------------------------------------
# Error shapes
# -------------------------------------------------------------------------
AuthError401:
type: object
description: Returned when mutual authentication fails (no or bad auth headers).
required: [status, code, message]
properties:
status:
type: string
enum: [error]
code:
type: string
enum: [UNAUTHORIZED, ERR_AUTH_FAILED]
message:
type: string
AuthError408:
type: object
description: |
Returned when the server is waiting for client certificates
and the 30-second timeout elapses.
required: [status, code, message]
properties:
status:
type: string
enum: [error]
code:
type: string
enum: [CERTIFICATE_TIMEOUT]
message:
type: string
AuthError500:
type: object
description: |
Returned when the server fails to sign its response payload
(`ERR_RESPONSE_SIGNING_FAILED`).
required: [status, code, description]
properties:
status:
type: string
enum: [error]
code:
type: string
enum: [ERR_RESPONSE_SIGNING_FAILED, ERR_INTERNAL_SERVER_ERROR]
description:
type: string
RequestAuthPayload:
type: object
description: |
Informational schema: the signed payload for a `general` REQUEST.
Assembled by `buildAuthMessageFromRequest` in `ExpressTransport`.
Wire encoding (binary, concatenated):
| Field | Encoding |
|------------------|------------------------------------------|
| requestId | 32 raw bytes (decoded from base64) |
| method | VarInt(len) + UTF-8 bytes |
| pathname | VarInt(len) + UTF-8 bytes |
| search | VarInt(len) + UTF-8 bytes, or VarInt(-1) |
| nHeaders | VarInt |
| headers (sorted) | VarInt(keyLen)+key + VarInt(valLen)+val |
| bodyLength | VarInt(len) or VarInt(-1) |
| body | raw bytes |
Only these headers are included (sorted, lowercase):
- Headers starting with `x-bsv-` (but NOT `x-bsv-auth-*`)
- `content-type` (normalized: type only, no parameters)
- `authorization`
BRC-104 v0.1 does not encode the URI scheme or authority (`Host`),
cookies, forwarding headers, or arbitrary standard headers. Those
values are outside this signature. A receiver must pin authority at a
trusted edge and must not choose a tenant or grant authority from
omitted metadata. Carry any security-relevant application value in an
exact signed `x-bsv-*` or `authorization` field and use distinct server
identity keys for virtual authorities that are separate principals.
ResponseAuthPayload:
type: object
description: |
Informational schema: the signed payload for a `general` RESPONSE.
Assembled by `buildResponsePayload` in `ExpressTransport`.
Wire encoding (binary, concatenated):
| Field | Encoding |
|------------------|------------------------------------------|
| requestId | 32 raw bytes (decoded from base64) |
| statusCode | VarInt |
| nHeaders | VarInt |
| headers (sorted) | VarInt(keyLen)+key + VarInt(valLen)+val |
| bodyLength | VarInt(len) or VarInt(-1) |
| body | raw bytes |
Only these response headers are included (sorted, lowercase):
- Headers starting with `x-bsv-` (but NOT `x-bsv-auth-*`)
- `authorization`
Standard response metadata such as `location`, `set-cookie`, and
`content-type` is outside this signature. Do not encode an
authenticated authorization decision solely in omitted response
metadata.
# ---------------------------------------------------------------------------
# Channels
# ---------------------------------------------------------------------------
channels:
wellKnownAuth:
address: '/.well-known/auth'
description: |
HTTP channel used for Phase 1 (non-general) BRC-103 handshake messages.
The client POSTs an `AuthMessage` JSON body; the server replies with an
`AuthMessage` JSON body. General-message header requirements do not apply.
In Socket.IO transports the same exchange happens over the `authMessage`
Socket.IO event (see `authsocket-asyncapi.yaml`) rather than this HTTP
endpoint.
messages:
initialRequest:
name: initialRequest
summary: Client initiates the BRC-103 handshake.
description: |
The client sends its identity, initial nonce, protocol version and
requested certificate set as an unsigned initialRequest JSON body.
headers:
$ref: '#/components/schemas/InitialRequestHeaders'
payload:
$ref: '#/components/schemas/AuthMessage'
examples:
- name: example-initial-request
payload:
version: '0.1'
messageType: 'initialRequest'
identityKey: '0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'
initialNonce: 'Z9+3SZ9qiJvtpQN1RNmmGue247vPuQLeqfIV2eFIMtvRRqFb0FpYckzLbawd0bdR'
requestedCertificates: { 'certifiers': [], 'types': {} }
initialResponse:
name: initialResponse
summary: Server completes Phase 1 of the BRC-103 handshake.
description: |
The server returns its initial nonce, the echoed client nonce and a
signature over that pair in the AuthMessage JSON body. The body also
carries requestedCertificates and any supplied certificates. The
client verifies the response; no initialRequest signature is required.
headers:
$ref: '#/components/schemas/InitialResponseHeaders'
payload:
$ref: '#/components/schemas/AuthMessage'
generalRequest:
address: '{applicationPath}'
description: |
Every authenticated application HTTP request (Phase 2). The path is
the actual application endpoint (e.g. `/sendMessage`, `/listMessages`).
The auth headers are attached alongside any application-specific headers.
parameters:
applicationPath:
description: The application route path (e.g. /sendMessage).
messages:
generalRequestMessage:
name: generalRequestMessage
summary: Authenticated application request.
description: |
The client sends application-level request headers and body, augmented
with `x-bsv-auth-*` mutual-auth headers. The `x-bsv-auth-signature`
covers the method, path, query string, declared signed-header subset,
and body as described in `RequestAuthPayload`. Authority, cookies,
forwarding metadata, and other standard headers are not covered by
the v0.1 frame.
headers:
$ref: '#/components/schemas/GeneralRequestHeaders'
payload:
description: Application-defined request body (any content type).
generalResponse:
address: '{applicationPath}'
description: |
Every authenticated application HTTP response (Phase 2). The server
signs the response status code, relevant headers, and body before sending.
parameters:
applicationPath:
description: The application route path.
messages:
generalResponseMessage:
name: generalResponseMessage
summary: Authenticated application response.
description: |
The server buffers the handler's response via `ResponseWriterWrapper`,
calls `Peer.toPeer` to sign it, and flushes the signed response
including `x-bsv-auth-*` response headers. The `x-bsv-auth-signature`
covers `requestId || statusCode || signed response headers || body`.
headers:
$ref: '#/components/schemas/GeneralResponseHeaders'
payload:
description: Application-defined response body.
authError:
address: '{applicationPath}'
description: |
Error responses emitted by the auth middleware when authentication fails.
parameters:
applicationPath:
description: The path at which the error was encountered.
messages:
unauthorized:
name: unauthorized
summary: 401 — mutual auth failed (no or bad auth headers).
payload:
$ref: '#/components/schemas/AuthError401'
certificateTimeout:
name: certificateTimeout
summary: 408 — server waited 30 s for client certificates and timed out.
payload:
$ref: '#/components/schemas/AuthError408'
signingFailed:
name: signingFailed
summary: 500 — server failed to sign its response payload.
payload:
$ref: '#/components/schemas/AuthError500'
# ---------------------------------------------------------------------------
# Operations
# ---------------------------------------------------------------------------
operations:
sendInitialRequest:
action: send
channel:
$ref: '#/channels/wellKnownAuth'
summary: Client initiates BRC-103 handshake (Phase 1, step 1).
description: |
Client POSTs an `initialRequest` AuthMessage to `/.well-known/auth`.
The body is a JSON-serialized `AuthMessage` with `messageType: initialRequest`.
messages:
- $ref: '#/channels/wellKnownAuth/messages/initialRequest'
receiveInitialResponse:
action: receive
channel:
$ref: '#/channels/wellKnownAuth'
summary: Client receives the server's Phase 1 challenge response.
description: |
Server replies with `initialResponse`. If `requestedCertificates` is
non-empty the client must send a follow-up request with the required
certificates before proceeding to Phase 2.
messages:
- $ref: '#/channels/wellKnownAuth/messages/initialResponse'
sendGeneralRequest:
action: send
channel:
$ref: '#/channels/generalRequest'
summary: Client sends an authenticated application request (Phase 2).
description: |
Any application HTTP request after the handshake. The client attaches
`x-bsv-auth-*` headers and a fresh signed payload covering the full
request. The server uses `buildAuthMessageFromRequest` to reconstruct
and verify the payload.
messages:
- $ref: '#/channels/generalRequest/messages/generalRequestMessage'
receiveGeneralResponse:
action: receive
channel:
$ref: '#/channels/generalResponse'
summary: Client receives the server's authenticated application response (Phase 2).
description: |
The server's response includes `x-bsv-auth-*` headers and a signature
over the response payload. The client can verify the response origin
using `buildResponsePayload` semantics.
messages:
- $ref: '#/channels/generalResponse/messages/generalResponseMessage'
receiveAuthError:
action: receive
channel:
$ref: '#/channels/authError'
summary: Client receives an auth error from the middleware.
messages:
- $ref: '#/channels/authError/messages/unauthorized'
- $ref: '#/channels/authError/messages/certificateTimeout'
- $ref: '#/channels/authError/messages/signingFailed'