# BSV Auth Module
> BRC-31 Peer-to-Peer Authentication for the BSV Rust SDK
## Overview
This module provides peer-to-peer authentication using the BRC-31 (Authrite) protocol, with certificate-based identity verification implementing BRC-52/53 and HTTP transport via BRC-104.
## Files
| `mod.rs` | Module root with re-exports (incl. WebSocket gated exports) | 85 |
| `types.rs` | Core types (AuthMessage, PeerSession, MessageType) | 457 |
| `session_manager.rs` | Session management with dual indexing | 444 |
| `peer.rs` | Core Peer implementation with `start()` transport setup | 996 |
| `certificates/` | Certificate submodule | - |
| `transports/` | Transport layer implementations (HTTP, WebSocket, Mock) | - |
| `utils/` | Utility functions | - |
## Submodules
### certificates/
Certificate types for BRC-52/53 selective disclosure:
- `mod.rs` - Re-exports and constants
- `certificate.rs` - Base Certificate type with signing/verification
- `master.rs` - MasterCertificate with field encryption and keyring management
- `verifiable.rs` - VerifiableCertificate with verifier-specific keyring
### transports/
Transport layer implementations for auth messages:
- `mod.rs` - Transport trait and re-exports (conditionally exports WebSocket types)
- `http.rs` - SimplifiedFetchTransport for HTTP (BRC-104), MockTransport for testing, HttpRequest/HttpResponse types
- `websocket_transport.rs` - WebSocketTransport for full-duplex WebSocket communication (requires `websocket` feature)
- See `transports/CLAUDE.md` for detailed transport documentation
### utils/
Utility functions:
- `mod.rs` - Re-exports
- `nonce.rs` - Nonce creation and verification
- `validation.rs` - Certificate validation, matching, encoding validation, and request set validation
## Constants
```rust
pub const AUTH_VERSION: &str = "0.1";
pub const AUTH_PROTOCOL_ID: &str = "auth message signature";
```
## Key Exports
Re-exports from `mod.rs`:
- `Certificate`, `MasterCertificate`, `VerifiableCertificate` (certificates)
- `Peer`, `PeerOptions` (peer)
- `SessionManager` (session_manager)
- `MockTransport`, `SimplifiedFetchTransport`, `Transport` (transports)
- `headers`, `TransportCallback`, `HttpRequest`, `HttpResponse` (transports)
- `WebSocketTransport`, `WebSocketTransportOptions` (transports, gated behind `websocket` feature)
- `current_time_ms`, `AuthMessage`, `MessageType`, `PeerSession`, `RequestedCertificateSet`, `AUTH_PROTOCOL_ID`, `AUTH_VERSION` (types)
- `create_nonce`, `validate_certificate_encoding`, `validate_certificates`, `validate_requested_certificate_set`, `verify_nonce` (utils)
### Types
**MessageType** - Authentication message types:
- `InitialRequest` - Initiates handshake
- `InitialResponse` - Responds to handshake
- `CertificateRequest` - Requests certificates
- `CertificateResponse` - Sends certificates
- `General` - Authenticated message
```rust
impl MessageType {
pub fn as_str(&self) -> &'static str
pub fn from_str(s: &str) -> Option<Self>
pub fn is_handshake(&self) -> bool // InitialRequest or InitialResponse
pub fn is_certificate(&self) -> bool // CertificateRequest or CertificateResponse
}
impl Display for MessageType { ... } // Outputs as_str() format
```
**AuthMessage** - Full authentication message structure:
```rust
pub struct AuthMessage {
pub version: String, // "0.1"
pub message_type: MessageType,
pub identity_key: PublicKey,
pub nonce: Option<String>, // base64
pub initial_nonce: Option<String>,
pub your_nonce: Option<String>,
pub certificates: Option<Vec<VerifiableCertificate>>,
pub requested_certificates: Option<RequestedCertificateSet>,
pub payload: Option<Vec<u8>>,
pub signature: Option<Vec<u8>>, // DER
}
impl AuthMessage {
pub fn new(message_type: MessageType, identity_key: PublicKey) -> Self
pub fn signing_data(&self) -> Vec<u8>
pub fn get_key_id(&self, peer_session_nonce: Option<&str>) -> String
pub fn validate(&self) -> Result<()>
}
```
**validate()** field requirements by message type:
- `InitialRequest` - requires `initial_nonce`
- `InitialResponse` - requires (`nonce` OR `initial_nonce`) + `your_nonce` + `signature`. Accepts either `nonce` or `initial_nonce` for cross-SDK compat (Go sends both, TS only sends `initialNonce`)
- `CertificateRequest` - requires `requested_certificates`
- `CertificateResponse` - requires `certificates`
- `General` - requires `signature`
**signing_data()** behavior by message type:
- `InitialRequest` - Empty (not signed, starts handshake)
- `InitialResponse` - `your_nonce || initial_nonce` (base64-decoded, initiator nonce then responder nonce; matches Go/TS)
- `General` - The payload bytes
- `CertificateRequest` - JSON-serialized `requested_certificates`
- `CertificateResponse` - JSON-serialized `certificates`
**get_key_id()** behavior by message type:
- `InitialResponse` - `"{your_nonce} {initial_nonce}"` (uses the message's own fields, ignoring `peer_session_nonce` param)
- All others - `"{nonce} {peer_session_nonce}"` (message nonce + the passed-in peer nonce)
**PeerSession** - Session state between peers:
```rust
pub struct PeerSession {
pub is_authenticated: bool,
pub session_nonce: Option<String>,
pub peer_nonce: Option<String>,
pub peer_identity_key: Option<PublicKey>,
pub last_update: u64,
pub certificates_required: bool,
pub certificates_validated: bool,
}
impl PeerSession {
pub fn new() -> Self
pub fn with_nonce(session_nonce: String) -> Self
pub fn touch(&mut self) // Update last_update to now
pub fn is_ready(&self) -> bool // Authenticated and certs validated (if required)
}
```
**RequestedCertificateSet** - Certificate request specification:
```rust
pub struct RequestedCertificateSet {
pub certifiers: Vec<String>, // hex-encoded public keys
pub types: HashMap<String, Vec<String>>, // type_id -> field_names
}
impl RequestedCertificateSet {
pub fn new() -> Self
pub fn is_empty(&self) -> bool
pub fn add_certifier(&mut self, certifier_hex: impl Into<String>)
pub fn add_type(&mut self, type_id: impl Into<String>, fields: Vec<String>)
pub fn is_certifier_trusted(&self, certifier_hex: &str) -> bool
pub fn is_type_requested(&self, type_id: &str) -> bool
pub fn get_fields_for_type(&self, type_id: &str) -> Option<&Vec<String>>
}
```
**current_time_ms** - Utility function:
```rust
pub fn current_time_ms() -> u64 // Milliseconds since Unix epoch
```
### Session Manager
**SessionManager** - Manages concurrent sessions with dual indexing:
```rust
impl SessionManager {
pub fn new() -> Self
pub fn add_session(&mut self, session: PeerSession) -> Result<()>
pub fn update_session(&mut self, session: PeerSession)
pub fn get_session(&self, identifier: &str) -> Option<&PeerSession>
pub fn get_session_mut(&mut self, session_nonce: &str) -> Option<&mut PeerSession>
pub fn remove_session(&mut self, session: &PeerSession)
pub fn remove_by_nonce(&mut self, session_nonce: &str)
pub fn has_session(&self, identifier: &str) -> bool
pub fn len(&self) -> usize
pub fn is_empty(&self) -> bool
pub fn get_sessions_for_identity(&self, identity_key_hex: &str) -> Vec<&PeerSession>
pub fn iter(&self) -> impl Iterator<Item = &PeerSession>
pub fn clear(&mut self)
pub fn prune_stale_sessions(&mut self, max_age_ms: u64) -> usize
}
```
Session lookup (`get_session`):
1. First tries as session nonce (exact match)
2. Then tries as identity key (returns best session)
3. "Best" = authenticated preferred, then most recent
Note: `get_session_mut` only searches by session nonce (not identity key).
### Peer
**Callback types**:
```rust
pub type GeneralMessageCallback = Box<dyn Fn(PublicKey, Vec<u8>) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> + Send + Sync>;
pub type CertificateCallback = Box<dyn Fn(PublicKey, Vec<VerifiableCertificate>) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> + Send + Sync>;
pub type CertificateRequestCallback = Box<dyn Fn(PublicKey, RequestedCertificateSet) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> + Send + Sync>;
```
**Peer** - Main authentication handler:
```rust
impl<W: WalletInterface, T: Transport> Peer<W, T> {
pub fn new(options: PeerOptions<W, T>) -> Self
pub fn start(&self) // Sets up client-side transport callback (ignores InitialRequest)
pub fn start_server(self: &Arc<Self>) // Sets up server-side transport callback (handles ALL message types)
// Sending messages
pub async fn to_peer(&self, message: &[u8], identity_key: Option<&str>, max_wait_time: Option<u64>) -> Result<()>
pub async fn get_authenticated_session(&self, identity_key: Option<&str>, max_wait_time: Option<u64>) -> Result<PeerSession>
pub async fn request_certificates(&self, requested: RequestedCertificateSet, identity_key: Option<&str>, max_wait_time: Option<u64>) -> Result<()>
pub async fn send_certificate_response(&self, verifier_identity_key: &str, certificates: Vec<VerifiableCertificate>) -> Result<()>
// Listening for general messages
pub async fn listen_for_general_messages<F>(&self, callback: F) -> u32
pub async fn stop_listening_for_general_messages(&self, callback_id: u32)
// Listening for certificates
pub async fn listen_for_certificates_received<F>(&self, callback: F) -> u32
pub async fn stop_listening_for_certificates_received(&self, callback_id: u32)
pub async fn listen_for_certificates_requested<F>(&self, callback: F) -> u32
pub async fn stop_listening_for_certificates_requested(&self, callback_id: u32)
// Session and identity
pub fn session_manager(&self) -> &Arc<RwLock<SessionManager>>
pub async fn get_identity_key(&self) -> Result<PublicKey>
pub async fn handle_incoming_message(&self, message: AuthMessage) -> Result<()>
}
```
**Internal methods** (private):
- `sign_message()` - Signs an AuthMessage using BRC-42 key derivation with the session's peer identity key
- `verify_message_signature()` - Verifies an incoming AuthMessage signature
- `initiate_handshake()` - Creates session, sends InitialRequest, waits for InitialResponse via oneshot channel with timeout
- `process_initial_request()` - Server-side: creates session, sends InitialResponse, optionally sends certificates
- `process_initial_response()` - Client-side: delegates to `process_initial_response_inner()`, sends errors through oneshot channel
- `process_initial_response_inner()` - Verifies InitialResponse signature, updates session, resolves pending handshake
- `process_certificate_request()` - Verifies signature, notifies certificate request callbacks
- `process_certificate_response()` - Verifies signature, validates certificates, updates session, notifies callbacks
- `process_general_message()` - Verifies auth and signature, updates peer nonce, notifies callbacks
**PeerOptions** - Configuration for creating a Peer:
```rust
pub struct PeerOptions<W: WalletInterface, T: Transport> {
pub wallet: W,
pub transport: T,
pub certificates_to_request: Option<RequestedCertificateSet>,
pub session_manager: Option<SessionManager>,
pub auto_persist_last_session: bool,
pub originator: Option<String>,
}
```
### Certificates
See `certificates/CLAUDE.md` for full API details. Key types:
- **Certificate** - Base certificate (BRC-52): `cert_type`, `serial_number`, `subject`, `certifier`, `fields` (encrypted), `signature`. Methods: `new()`, `sign()`, `verify()`, `to_binary()`/`from_binary()`, field encryption key ID helpers.
- **MasterCertificate** - Wraps `Certificate` + `master_keyring`. Methods: `create_certificate_fields()`, `create_keyring_for_verifier()`, `issue_for_subject()`, `decrypt_field()`/`decrypt_fields()`. Derefs to `Certificate`.
- **VerifiableCertificate** - Wraps `Certificate` + verifier `keyring`. Methods: `decrypt_field()`/`decrypt_fields()`, `revealable_fields()`, `to_json_value()`, accessor methods (`subject()`, `certifier()`, etc.). Derefs to `Certificate`.
```rust
pub const CERTIFICATE_FIELD_ENCRYPTION_PROTOCOL: &str = "certificate field encryption";
pub const CERTIFICATE_SIGNATURE_PROTOCOL: &str = "certificate signature";
```
### Transport
See `transports/CLAUDE.md` for full API details.
**Transport** - Trait for message transport:
```rust
#[async_trait]
pub trait Transport: Send + Sync {
async fn send(&self, message: &AuthMessage) -> Result<()>;
fn set_callback(&self, callback: Box<TransportCallback>);
fn clear_callback(&self);
}
pub type TransportCallback = dyn Fn(AuthMessage) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> + Send + Sync;
```
Three implementations:
- **SimplifiedFetchTransport** - HTTP transport (BRC-104): `new(base_url)`, `message_to_headers()`, `headers_to_message_fields()`
- **WebSocketTransport** - Full-duplex WebSocket (requires `websocket` feature): lazy connect on first `send()`, background receive loop, JSON payloads, connection dropped on errors. Config via `WebSocketTransportOptions { base_url, read_deadline_secs }`
- **MockTransport** - Testing: `queue_response()`, `get_sent_messages()`, `receive_message()`
**HttpRequest** / **HttpResponse** - BRC-104 binary payload types for General messages (request_id, method/status, path, headers, body).
### Utils
**Nonce functions**: `create_nonce()`, `verify_nonce()`, `validate_nonce_format()`, `get_nonce_random()` (protocol: `"server hmac"`)
**Validation functions**: `validate_certificates()`, `validate_certificate()`, `get_verifiable_certificates()`, `certificates_match_request()`
**Encoding validation**: `validate_certificate_encoding()` (structural checks on cert fields), `validate_requested_certificate_set()` (well-formedness of request sets)
## Usage Examples
### Client-Side Peer Authentication
```rust
use bsv_rs::auth::{Peer, PeerOptions, SimplifiedFetchTransport};
use bsv_rs::wallet::ProtoWallet;
use bsv_rs::primitives::PrivateKey;
let wallet = ProtoWallet::new(Some(PrivateKey::random()));
let transport = SimplifiedFetchTransport::new("https://example.com");
let peer = Peer::new(PeerOptions {
wallet,
transport,
certificates_to_request: None,
session_manager: None,
auto_persist_last_session: false,
originator: Some("myapp.com".into()),
});
peer.start(); // Client-side: handles responses but not incoming InitialRequest
peer.to_peer(b"Hello, world!", None, None).await?;
```
### Server-Side Peer Authentication
```rust
use std::sync::Arc;
let peer = Arc::new(Peer::new(PeerOptions {
wallet,
transport,
certificates_to_request: None,
session_manager: None,
auto_persist_last_session: false,
originator: Some("myserver.com".into()),
}));
peer.start_server(); // Server-side: handles ALL messages including InitialRequest
```
### Listening for Messages
```rust
println!("From {}: {:?}", sender.to_hex(), payload);
Ok(())
})
}).await;
peer.stop_listening_for_general_messages(callback_id).await;
```
### Certificate Exchange
```rust
let mut requested = RequestedCertificateSet::new();
requested.add_certifier(trusted_certifier_hex);
requested.add_type(cert_type_base64, vec!["name".into(), "email".into()]);
peer.request_certificates(requested, Some(&peer_identity_key), None).await?;
```
### WebSocket Transport
```rust
use bsv_rs::auth::{Peer, PeerOptions, WebSocketTransport, WebSocketTransportOptions};
let transport = WebSocketTransport::new(WebSocketTransportOptions {
base_url: "wss://example.com/ws".to_string(),
read_deadline_secs: Some(60),
})?;
let peer = Peer::new(PeerOptions {
wallet,
transport,
certificates_to_request: None,
session_manager: None,
auto_persist_last_session: false,
originator: Some("myapp.com".into()),
});
peer.start();
```
## Protocol Details
### Peer Lifecycle
1. **Create**: `Peer::new(options)` - constructs peer with wallet and transport
2. **Start**: Choose one:
- `peer.start()` - client-side callback (handles InitialResponse, General, Certificate messages; ignores InitialRequest)
- `peer.start_server()` - server-side callback (handles ALL message types including InitialRequest). Requires `Arc<Peer>` since it routes through `handle_incoming_message()`.
3. **Communicate**: `to_peer()`, `request_certificates()`, etc.
### Authentication Flow
1. **InitialRequest**: Initiator sends `initial_nonce` (session nonce) + optional certificate request
2. **InitialResponse**: Responder sends `nonce` and `initial_nonce` (both set to responder's session nonce), `your_nonce` (initiator's nonce echoed back), and signature over `your_nonce || initial_nonce`
3. **CertificateExchange** (optional): Peers exchange verifiable certificates
4. **General**: Authenticated messages with payload, signed with per-message random nonce
### InitialResponse Field Mapping (Cross-SDK)
The InitialResponse field mapping matches Go and TS SDKs:
- `initial_nonce` = responder's session nonce (Go: `InitialNonce = session.SessionNonce`)
- `nonce` = responder's session nonce (same value, Go: `Nonce = ourNonce`)
- `your_nonce` = initiator's nonce echoed back (for session lookup by initiator)
On the initiator side, the responder's session nonce (`initial_nonce`) is stored as `peer_nonce`.
**TS SDK compatibility**: The TS SDK's InitialResponse has NO `nonce` field (only `initialNonce`, `yourNonce`). The Go SDK sends both `Nonce` and `InitialNonce`. The Rust SDK accepts either: both `start()` callback and `process_initial_response()` fall back to `initial_nonce` when `nonce` is absent.
### Error Propagation
Errors during InitialResponse processing (e.g., signature verification failure) are sent through the oneshot channel to the waiting `initiate_handshake()` caller. This is implemented in two places:
1. **`start()` callback**: Extracts nonces in a closure; on error, sends through oneshot before returning
2. **`process_initial_response()`**: Delegates to `process_initial_response_inner()`; on error, sends through oneshot
Without this, errors would be swallowed by the routing task and the caller would time out instead of receiving the actual error.
### Nonce Generation
Message nonces for General/CertificateRequest/CertificateResponse use simple 32-byte random values (base64-encoded), matching TypeScript's `Utils.toBase64(Random(32))`. Session nonces use HMAC-based `create_nonce()` with counterparty=Self (None).
Nonce verification on InitialResponse uses `nonce` if present, falls back to `initial_nonce` for TS SDK compat. Verification failure is non-fatal (matches Go/TS behavior).
### Cryptographic Details
- **Signature**: Protocol `"auth message signature"`, security level 2 (Counterparty), key ID `"{nonce} {peer_session_nonce}"`
- **InitialResponse key ID**: `"{your_nonce} {initial_nonce}"` (uses message's own fields)
- **Nonce**: 32 bytes (16 random + 16 HMAC via `"server hmac"` protocol), base64 encoded
- **Certificate fields**: Protocol `"certificate field encryption"`, security level 2. Master key ID: `"{field_name}"`, verifiable key ID: `"{serial_number_base64} {field_name}"`
## BRC-104 HTTP Headers
Headers: `x-bsv-auth-version`, `x-bsv-auth-identity-key` (hex), `x-bsv-auth-nonce` (base64), `x-bsv-auth-your-nonce`, `x-bsv-auth-signature` (base64), `x-bsv-auth-message-type`, `x-bsv-auth-request-id` (base64), `x-bsv-auth-requested-certificates` (JSON).
Payload format for General messages: **Request** `[request_id: 32][method: varint+str][path: varint+str][search: varint+str][headers: varint+pairs][body: varint+bytes]`, **Response** `[request_id: 32][status: varint][headers: varint+pairs][body: varint+bytes]`.
## Error Types
| `AuthError(String)` | General authentication error |
| `SessionNotFound(String)` | Session not found by identifier |
| `CertificateValidationError(String)` | Certificate validation failed |
| `TransportError(String)` | Transport layer error |
| `InvalidNonce(String)` | Nonce format or verification error |
## Feature Flags
```toml
[dependencies]
bsv-rs = { version = "0.3", features = ["auth"] }
# With HTTP transport
bsv-rs = { version = "0.3", features = ["auth", "http"] }
# With WebSocket transport
bsv-rs = { version = "0.3", features = ["auth", "websocket"] }
```
The `auth` feature requires `wallet` and `messages` features (included automatically).
The `websocket` feature is opt-in (not included in `full`) and adds `tokio-tungstenite`, `futures-util`, and `url` dependencies.
## Cross-SDK Compatibility
This module maintains API compatibility with:
- [TypeScript SDK](https://github.com/bitcoin-sv/ts-sdk) - `Peer`, `SessionManager`, `Certificate`
- [Go SDK](https://github.com/bitcoin-sv/go-sdk) - `Peer`, `SessionManager`, `Certificate`
Authentication messages and certificates are wire-compatible across all SDK implementations.
Key compatibility details:
- InitialResponse `signing_data` signs `your_nonce || initial_nonce` (matches Go/TS byte order)
- InitialResponse `get_key_id` uses `"{your_nonce} {initial_nonce}"` format (matches Go `keyID()`)
- `create_nonce` uses counterparty=Self (matches Go/TS)
- Nonce verification on InitialResponse is optional (non-fatal failure, matches Go/TS behavior)
## Dependencies
The auth module uses:
- `tokio` - Async runtime (sync primitives, time, spawn)
- `async_trait` - Async trait support
- `serde`, `serde_json` - Serialization
- `rand` - Random number generation
- `reqwest` - HTTP client (with `http` feature)
- `tokio-tungstenite` - WebSocket client (with `websocket` feature)
- `futures-util` - Stream/Sink utilities for WebSocket (with `websocket` feature)
- Wallet module for key operations
- Primitives module for cryptography
## Testing
Run auth module tests:
```bash
cargo test --features auth
```
Run with HTTP transport tests:
```bash
cargo test --features auth,http
```
Run with WebSocket transport tests:
```bash
cargo test --features auth,websocket
```
**Test pattern note**: `ChannelTransport` (MockTransport) in tests never invokes the `start()` callback. Messages go through `handle_incoming_message` → `process_initial_response`, NOT through the `start()` callback. The `start()` callback path is only exercised by real transports (HTTP, WebSocket) that invoke the registered callback when they receive data.
## Related Documentation
- `transports/CLAUDE.md` - Transport layer implementations
- `../wallet/CLAUDE.md` - Wallet module (WalletInterface)
- `../primitives/CLAUDE.md` - Cryptographic primitives