13 KiB
SigSocket: WebSocket Signing Server Architecture
Based on my analysis of the existing Actix application structure, I've designed a comprehensive architecture for implementing a WebSocket server that handles signing operations. This server will integrate seamlessly with the existing hostbasket application.
1. Overview
SigSocket will:
- Accept WebSocket connections from clients
- Allow clients to identify themselves with a public key
- Provide a
send_to_sign()
function that takes a public key and a message - Forward the message to the appropriate client for signing
- Wait for a signed response (with a 1-minute timeout)
- Verify the signature using the client's public key
- Return the response message and signature
2. Component Architecture
graph TD
A[Actix Web Server] --> B[SigSocket Manager]
B --> C[Connection Registry]
B --> D[Message Handler]
D --> E[Signature Verifier]
F[Client] <--> B
G[Application Code] --> H[SigSocketService]
H --> B
Key Components:
-
SigSocket Manager
- Handles WebSocket connections
- Manages connection lifecycle
- Routes messages to appropriate handlers
-
Connection Registry
- Maps public keys to active WebSocket connections
- Handles connection tracking and cleanup
- Provides lookup functionality
-
Message Handler
- Processes incoming messages
- Implements the message protocol
- Manages timeouts for responses
-
Signature Verifier
- Verifies signatures using public keys
- Implements cryptographic operations
- Ensures security of the signing process
-
SigSocket Service
- Provides a clean API for the application to use
- Abstracts WebSocket complexity from business logic
- Handles error cases and timeouts
3. Directory Structure
src/
├── lib.rs # Main library exports
├── manager.rs # WebSocket connection manager
├── registry.rs # Connection registry
├── handler.rs # Message handling logic
├── protocol.rs # Message protocol definitions
├── crypto.rs # Cryptographic operations
└── service.rs # Service API
4. Data Flow
sequenceDiagram
participant Client
participant SigSocketManager
participant Registry
participant Application
participant SigSocketService
Client->>SigSocketManager: Connect
Client->>SigSocketManager: Introduce(public_key)
SigSocketManager->>Registry: Register(connection, public_key)
Application->>SigSocketService: send_to_sign(public_key, message)
SigSocketService->>Registry: Lookup(public_key)
Registry-->>SigSocketService: connection
SigSocketService->>SigSocketManager: Send message to connection
SigSocketManager->>Client: Message to sign
Client->>SigSocketManager: Signed response
SigSocketManager->>SigSocketService: Forward response
SigSocketService->>SigSocketService: Verify signature
SigSocketService-->>Application: Return verified response
5. Message Protocol
We'll define a minimalist protocol for communication:
// Client introduction (first message upon connection)
<base64_encoded_public_key>
// Sign request (sent from server to client)
<base64_encoded_message>
// Sign response (sent from client to server)
<base64_encoded_message>.<base64_encoded_signature>
This simplified protocol reduces overhead and complexity:
- The introduction is always the first message, containing only the public key
- Sign requests contain only the message to be signed
- Responses use a simple "message.signature" format
6. Required Dependencies
We'll need to add the following dependencies to the project:
# WebSocket support
actix-web-actors = "4.2.0"
# Cryptography
secp256k1 = "0.28.0" # For secp256k1 signatures (used in Bitcoin/Ethereum)
sha2 = "0.10.8" # For hashing before signing
hex = "0.4.3" # For hex encoding/decoding
base64 = "0.21.0" # For base64 encoding/decoding
rand = "0.8.5" # For generating random data
7. Implementation Details
7.1 SigSocket Manager
The SigSocket Manager will handle the lifecycle of WebSocket connections:
pub struct SigSocketManager {
registry: Arc<RwLock<ConnectionRegistry>>,
}
impl Actor for SigSocketManager {
type Context = ws::WebsocketContext<Self>;
fn started(&mut self, ctx: &mut Self::Context) {
// Handle connection start
}
fn stopped(&mut self, ctx: &mut Self::Context) {
// Handle connection close and cleanup
}
}
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for SigSocketManager {
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
// Handle different types of WebSocket messages
}
}
7.2 Connection Registry
The Connection Registry will maintain a mapping of public keys to active connections:
pub struct ConnectionRegistry {
connections: HashMap<String, Addr<SigSocketManager>>,
}
impl ConnectionRegistry {
pub fn register(&mut self, public_key: String, addr: Addr<SigSocketManager>) {
self.connections.insert(public_key, addr);
}
pub fn unregister(&mut self, public_key: &str) {
self.connections.remove(public_key);
}
pub fn get(&self, public_key: &str) -> Option<&Addr<SigSocketManager>> {
self.connections.get(public_key)
}
}
7.3 SigSocket Service
The SigSocket Service will provide a clean API for controllers:
pub struct SigSocketService {
registry: Arc<RwLock<ConnectionRegistry>>,
}
impl SigSocketService {
pub async fn send_to_sign(&self, public_key: &str, message: &[u8])
-> Result<(Vec<u8>, Vec<u8>), SigSocketError> {
// 1. Find the connection for the public key
let connection = self.registry.read().await.get(public_key).cloned();
if let Some(conn) = connection {
// 2. Create a response channel
let (tx, rx) = oneshot::channel();
// 3. Register the response channel with the connection
self.pending_requests.write().await.insert(conn.clone(), tx);
// 4. Send the message to the client (just the raw message)
conn.do_send(base64::encode(message));
// 5. Wait for the response with a timeout
match tokio::time::timeout(Duration::from_secs(60), rx).await {
Ok(Ok(response)) => {
// 6. Parse the response in format "message.signature"
let parts: Vec<&str> = response.split('.').collect();
if parts.len() != 2 {
return Err(SigSocketError::InvalidResponseFormat);
}
let message_b64 = parts[0];
let signature_b64 = parts[1];
// 7. Decode the message and signature
let message_bytes = match base64::decode(message_b64) {
Ok(m) => m,
Err(_) => return Err(SigSocketError::DecodingError),
};
let signature_bytes = match base64::decode(signature_b64) {
Ok(s) => s,
Err(_) => return Err(SigSocketError::DecodingError),
};
// 8. Verify the signature
if self.verify_signature(&signature_bytes, &message_bytes, public_key) {
Ok((message_bytes, signature_bytes))
} else {
Err(SigSocketError::InvalidSignature)
}
},
Ok(Err(_)) => Err(SigSocketError::ChannelClosed),
Err(_) => Err(SigSocketError::Timeout),
}
} else {
Err(SigSocketError::ConnectionNotFound)
}
}
}
8. Error Handling
We'll define a comprehensive error type for the SigSocket service:
#[derive(Debug, thiserror::Error)]
pub enum SigSocketError {
#[error("Connection not found for the provided public key")]
ConnectionNotFound,
#[error("Timeout waiting for signature")]
Timeout,
#[error("Invalid signature")]
InvalidSignature,
#[error("Channel closed unexpectedly")]
ChannelClosed,
#[error("Invalid response format, expected 'message.signature'")]
InvalidResponseFormat,
#[error("Error decoding base64 message or signature")]
DecodingError,
#[error("Invalid public key format")]
InvalidPublicKey,
#[error("Invalid signature format")]
InvalidSignature,
#[error("Internal cryptographic error")]
InternalError,
#[error("WebSocket error: {0}")]
WebSocketError(#[from] ws::ProtocolError),
#[error("Base64 decoding error: {0}")]
Base64Error(#[from] base64::DecodeError),
#[error("Hex decoding error: {0}")]
HexError(#[from] hex::FromHexError),
}
9. Cryptographic Operations
The SigSocket service uses the secp256k1 elliptic curve for cryptographic operations, which is the same curve used in Bitcoin and Ethereum. This makes it compatible with many blockchain applications and wallets.
use secp256k1::{Secp256k1, Message, PublicKey, Signature};
use sha2::{Sha256, Digest};
pub fn verify_signature(public_key_hex: &str, message: &[u8], signature_hex: &str) -> Result<bool, SigSocketError> {
// 1. Parse the public key
let public_key_bytes = hex::decode(public_key_hex)
.map_err(|_| SigSocketError::InvalidPublicKey)?;
let public_key = PublicKey::from_slice(&public_key_bytes)
.map_err(|_| SigSocketError::InvalidPublicKey)?;
// 2. Parse the signature
let signature_bytes = hex::decode(signature_hex)
.map_err(|_| SigSocketError::InvalidSignature)?;
let signature = Signature::from_compact(&signature_bytes)
.map_err(|_| SigSocketError::InvalidSignature)?;
// 3. Hash the message (secp256k1 requires a 32-byte hash)
let mut hasher = Sha256::new();
hasher.update(message);
let message_hash = hasher.finalize();
// 4. Create a secp256k1 message from the hash
let secp_message = Message::from_slice(&message_hash)
.map_err(|_| SigSocketError::InternalError)?;
// 5. Verify the signature
let secp = Secp256k1::verification_only();
match secp.verify(&secp_message, &signature, &public_key) {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}
This implementation:
- Decodes the hex-encoded public key and signature
- Hashes the message using SHA-256 (required for secp256k1)
- Verifies the signature using the secp256k1 library
- Returns a boolean indicating whether the signature is valid
10. Security Considerations
- Public Key Validation: Validate public keys upon connection to ensure they are properly formatted secp256k1 keys
- Message Authentication: Consider adding a nonce or timestamp to prevent replay attacks
- Rate Limiting: Implement rate limiting to prevent DoS attacks
- Connection Timeouts: Automatically close inactive connections
- Error Logging: Log errors but avoid exposing sensitive information
- Input Validation: Validate all inputs to prevent injection attacks
- Secure Hashing: Always hash messages before signing to prevent length extension attacks
11. Testing Strategy
- Unit Tests: Test individual components in isolation
- Integration Tests: Test the interaction between components
- End-to-End Tests: Test the complete flow from client connection to signature verification
- Load Tests: Test the system under high load to ensure stability
- Security Tests: Test for common security vulnerabilities
12. Integration with Existing Code
The SigSocketService can be used by any part of the application that needs signing functionality:
// Create and initialize the service
let registry = Arc::new(RwLock::new(ConnectionRegistry::new()));
let sigsocket_service = Arc::new(SigSocketService::new(registry.clone()));
// Use the service to send a message for signing
async fn sign_message(service: Arc<SigSocketService>, public_key: String, message: Vec<u8>) -> Result<(Vec<u8>, Vec<u8>), SigSocketError> {
service.send_to_sign(&public_key, &message).await
}
// Example usage in an HTTP handler
async fn handle_sign_request(
service: web::Data<Arc<SigSocketService>>,
req: web::Json<SignRequest>,
) -> HttpResponse {
match service.send_to_sign(&req.public_key, &req.message).await {
Ok((response, signature)) => {
HttpResponse::Ok().json(json!({
"response": base64::encode(response),
"signature": base64::encode(signature),
}))
},
Err(e) => {
HttpResponse::InternalServerError().json(json!({
"error": e.to_string(),
}))
}
}
}
13. Deployment Considerations
- Scalability: The SigSocket server should be designed to scale horizontally
- Monitoring: Implement metrics for connection count, message throughput, and error rates
- Logging: Log important events for debugging and auditing
- Documentation: Document the API and protocol for client implementers