Auth
The BSV::Auth module implements three complementary BSV authentication protocols:
| Class | BRC | Role |
|---|---|---|
Auth::AuthFetch | BRC-104 | Authenticated HTTP client |
Auth::Peer / Auth::PeerSession | BRC-103 | Mutual-auth message protocol |
Auth::Certificate / Auth::MasterCertificate | BRC-52 + BRC-53 | Identity certificate format and selective disclosure |
Auth::AuthMiddleware | BRC-104 server side | Rack adapter |
Boundary note: this page covers the Auth protocol layer (BRC-103 mutual auth, BRC-52/53 certificates, BRC-104 HTTP transport). The lower-level BRC-103 wire format for BRC-100 wallet calls is a separate concern documented in BRC-103 Wire Layer. Identity::Client builds on top of Auth certificates to publish and revoke on-chain identity tokens.
AuthFetch — BRC-104
Auth::AuthFetch is the primary client for authenticated HTTP. It wraps Peer internally and exposes a simple #fetch method. For most applications this is the only Auth class you need to interact with directly.
Construction
require 'bsv-sdk'
# wallet must be a BRC-100 interface (ProtoWallet or bsv-wallet Client)
client = BSV::Auth::AuthFetch.new(wallet: my_wallet)
Optional parameters:
session_mgr = BSV::Auth::SessionManager.new(default_ttl: 7200) # 2-hour TTL
client = BSV::Auth::AuthFetch.new(
wallet: my_wallet,
requested_certificates: { certifiers: ['02abc...'], types: { 'exOl3K...' => ['email'] } },
session_manager: session_mgr,
payment_max_retries: 5
)
Basic authenticated GET
response = client.fetch('https://api.example.com/resource')
puts response.status # => 200
puts response.body # => '{"data": "..."}'
puts response.identity_key # => '02abc...' — server's compressed public-key hex
AuthFetch handles the BRC-103 handshake automatically on the first request to each base URL. Subsequent requests to the same base URL reuse the cached session.
POST with a JSON body
response = client.fetch(
'https://api.example.com/items',
method: 'POST',
body: { name: 'example', value: 42 }, # Hash → JSON automatically
timeout: 60
)
402 Payment Required
When a server responds with HTTP 402, AuthFetch automatically:
- Reads the
x-bsv-payment-satoshis-required,x-bsv-payment-derivation-prefix, andx-bsv-identity-keyresponse headers. - Derives a payment key and creates a BSV transaction via
wallet.create_action. - Retries the original request with an
x-bsv-paymentheader containing the signed transaction.
This requires the wallet to implement get_public_key and create_action (i.e. a full wallet, not just ProtoWallet).
# AuthFetch handles 402 silently — the caller just sees the final response
response = client.fetch('https://api.example.com/paid-resource')
puts response.status # => 200 if payment succeeded, 402 if max retries exhausted
Session caching
Caching happens at two layers. AuthFetch caches a Peer instance per base URL — subsequent requests to the same host reuse the mutual-auth handshake. Each Peer in turn holds a SessionManager that stores authenticated sessions dual-indexed by session nonce and peer identity key (supporting multiple concurrent sessions per peer). The default session TTL is 3,600 seconds (one hour). Pass nil to disable expiry entirely — suitable for long-running daemons that connect to a fixed set of servers.
# No expiry
session_mgr = BSV::Auth::SessionManager.new(default_ttl: nil)
# Share one session pool across multiple AuthFetch instances
session_mgr = BSV::Auth::SessionManager.new(default_ttl: 3600)
client_a = BSV::Auth::AuthFetch.new(wallet: wallet, session_manager: session_mgr)
client_b = BSV::Auth::AuthFetch.new(wallet: wallet, session_manager: session_mgr)
Call session_mgr.sweep_expired periodically in long-running servers to reclaim memory.
Thread safety
AuthFetch is safe to use from multiple threads. The @peers hash (one Peer per base URL) is protected by a Mutex. Each in-flight request uses its own Queue to match the response to the originating call, so concurrent requests to the same server do not interfere.
SessionManager is independently thread-safe (all mutations are under a single Mutex).
Security: redact bodies in logging
Payment envelopes (x-bsv-payment) contain signed transactions. Application-level request and response bodies may carry sensitive credentials or private data. Never log full request or response bodies in production.
# Wrong — leaks payment transaction and response body to the log
response = client.fetch('https://api.example.com/resource')
logger.debug("response: #{response.body}")
# Right — log only the status and identity key; omit body content
response = client.fetch('https://api.example.com/resource')
logger.debug("auth fetch: status=#{response.status} peer=#{response.identity_key[0, 16]}...")
Peer / PeerSession — BRC-103
Auth::Peer implements the BRC-103 mutual-authentication message protocol. AuthFetch uses Peer internally; reach for Peer directly only when building custom transports or when you need to exchange arbitrary authenticated payloads between two parties.
BRC-103 is NOT BRC-31. BRC-31 was the earlier “Authrite” precursor. BRC-103 superseded it. The protocol version negotiated on the wire is '0.1'.
Handshake overview
Initiator Responder
────────── ─────────
Peer.new(wallet:, transport:) Peer.new(wallet:, transport:)
#to_peer(payload, peer_identity_key) →
Peer fires on_general_message callback
← (handshake + authenticated general message)
The handshake is two messages:
initialRequest— initiator sends identity key and session nonce.initialResponse— responder echoes nonce, signs over both nonces, sends its own nonce.
After the handshake, all subsequent messages are general messages signed over the payload with a per-message nonce.
Using Peer with a transport
You must supply a Transport — any object that includes BSV::Auth::Transport and implements #send and #on_data:
# Using the built-in SimplifiedFetchTransport (HTTP POST to /.well-known/auth)
transport = BSV::Auth::SimplifiedFetchTransport.new('https://server.example.com')
peer = BSV::Auth::Peer.new(wallet: my_wallet, transport: transport)
received = Queue.new
peer.on_general_message { |sender_key, payload| received.push([sender_key, payload]) }
# Send an authenticated message (handshake auto-initiated on first call)
peer.to_peer([72, 101, 108, 108, 111]) # "Hello" as byte array
sender_key, payload = received.pop
puts sender_key # => '02abc...' — server's identity key
puts payload.pack('C*') # => 'Hello' echoed back
Certificate negotiation
Pass certificates_to_request to require the peer to present certificates during the handshake. The peer must hold matching certificates or the connection is rejected.
TRUSTED_CERTIFIER = '02deadbeef...' # pin to a known certifier pubkey
peer = BSV::Auth::Peer.new(
wallet: my_wallet,
transport: transport,
certificates_to_request: {
certifiers: [TRUSTED_CERTIFIER],
types: { 'exOl3K...' => ['email'] }
}
)
peer.on_certificates_received do |sender_key, certs|
certs.each { |c| puts "received cert from #{c['certifier']}" }
end
Security: nonce sourcing
Nonces in BRC-103 are self-authenticating: 16 CSPRNG bytes followed by a 32-byte HMAC-SHA256 computed by the wallet. Only the wallet that created a nonce can verify it. They are not reusable and must never come from user input.
# Wrong — user-supplied nonce, breaks authentication guarantee
nonce = params[:nonce] # attacker controls this
session.session_nonce = nonce
# Right — let BSV::Auth::Nonce generate it
nonce = BSV::Auth::Nonce.create(my_wallet)
# Nonce.create is called internally by Peer; you do not need to call it directly
BSV::Auth::Nonce.create wraps SecureRandom.random_bytes(16) followed by wallet.create_hmac. Never substitute a counter, timestamp, or application-generated string.
Security: certifier-pubkey pinning
When verifying a peer’s certificate, always check the certifier against a known list. Trust-on-first-use (TOFU) — accepting whatever certifier the peer claims — defeats certificate verification entirely.
KNOWN_CERTIFIERS = [
'02deadbeef...', # production certifier
'03cafebabe...' # backup certifier
].freeze
peer.on_certificates_received do |sender_key, certs|
certs.each do |cert|
certifier = cert.is_a?(Hash) ? cert['certifier'] : cert.certifier
unless KNOWN_CERTIFIERS.include?(certifier)
raise BSV::Auth::AuthError, "Untrusted certifier: #{certifier}"
end
# proceed with cert.fields
end
end
Certificate / MasterCertificate — BRC-52 + BRC-53
Auth::Certificate (BRC-52) represents a signed identity certificate binding named fields to a subject public key. Auth::MasterCertificate (BRC-53) extends it with creation and selective-disclosure flows.
Two-layer encryption envelope
Each certificate field is encrypted with a two-layer scheme:
- Per-field symmetric key — a random AES-256-GCM key generated for each field independently.
- Wrapped symmetric key — the per-field key is re-encrypted via BRC-42 derivation:
- Protocol:
[2, 'certificate field encryption'] - Key ID for the master keyring:
field_name(no serial number) - Key ID for a verifier keyring:
"#{serial_number} #{field_name}"
- Protocol:
The serial-number scoping in the verifier key ID is what prevents cross-certificate replay: a keyring entry produced for certificate A cannot be used to decrypt certificate B, even if both share a field with the same name.
Issuing a certificate (certifier side)
certifier_wallet = BSV::Wallet::ProtoWallet.new(certifier_key)
cert_type = Base64.strict_encode64(SecureRandom.random_bytes(32))
cert = BSV::Auth::MasterCertificate.issue_certificate_for_subject(
certifier_wallet,
subject_pubkey_hex,
{ 'email' => 'alice@example.com', 'name' => 'Alice' },
cert_type
)
puts cert.serial_number # Base64-encoded 32-byte random serial
puts cert.signature # DER-encoded certifier signature (hex)
Selective disclosure — the default example
Selective disclosure is the normal case. To reveal a field to a verifier, re-wrap the per-field symmetric key for the verifier’s public key — no re-encryption of the field value itself:
# Subject holds the MasterCertificate and a wallet
subject_wallet = BSV::Wallet::ProtoWallet.new(subject_key)
verifier_pubkey = '02verifier...'
verifier_keyring = BSV::Auth::MasterCertificate.create_keyring_for_verifier(
subject_wallet,
certifier: cert.certifier,
verifier: verifier_pubkey,
fields: cert.fields,
fields_to_reveal: ['email'], # selective — only email, not name
master_keyring: cert.master_keyring,
serial_number: cert.serial_number
)
verifiable = BSV::Auth::VerifiableCertificate.from_certificate(cert, verifier_keyring)
# Transmit verifiable.to_h to the verifier
What happens under the hood:
- Subject decrypts the master keyring entry for
'email'(key ID:'email', counterparty: certifier pubkey). - Verifies the recovered symmetric key actually decrypts
cert.fields['email']. - Re-encrypts the symmetric key for the verifier (key ID:
"#{serial_number} email", counterparty: verifier pubkey). - The verifier receives the encrypted field value plus the re-wrapped key — no plaintext ever leaves the subject’s wallet.
Verifier decryption
verifier_wallet = BSV::Wallet::ProtoWallet.new(verifier_key)
vc = BSV::Auth::VerifiableCertificate.from_hash(received_hash)
# Verify the certifier signature before trusting any fields
unless vc.verify
raise 'Certificate signature invalid'
end
# Verify certifier identity against pinned list (see Security section)
unless KNOWN_CERTIFIERS.include?(vc.certifier)
raise "Untrusted certifier: #{vc.certifier}"
end
decrypted = vc.decrypt_fields(verifier_wallet)
puts decrypted['email'] # => 'alice@example.com'
Certificate signature verification
Signatures use BRC-42 derivation with:
- Protocol:
[2, 'certificate signature'] - Key ID:
"#{type} #{serial_number}" - Counterparty:
'anyone'on signing; certifier pubkey on verifying
valid = cert.verify
puts valid # => true
AuthMiddleware — server-side adapter
Auth::AuthMiddleware is a Rack middleware that adds BRC-104 server-side authentication to any Rack application. It intercepts BRC-103 handshake requests and authenticated general messages, passing everything else through to the downstream app.
# config.ru
require 'bsv-sdk'
app = proc do |env|
[200, { 'content-type' => 'application/json' }, ['{"status":"ok"}']]
end
wallet = BSV::Wallet::ProtoWallet.new(BSV::Primitives::PrivateKey.generate)
use BSV::Auth::AuthMiddleware,
wallet: wallet,
certificates_to_request: nil # omit to skip certificate requirements
run app
The middleware handles three categories of request:
| Request | Handling |
|---|---|
POST /.well-known/auth | BRC-103 handshake dispatch via internal Peer |
Request with x-bsv-auth-* headers | Signature verification; downstream app called; response signed |
| Everything else | Passed through unchanged |
See gem/bsv-sdk/lib/bsv/auth/auth_middleware.rb for the full implementation including the BridgeTransport that connects the HTTP request/response cycle to the internal Peer.