9.9 KiB
WebSocket Signing Server Architecture Plan
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
The WebSocket Signing Server 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[WebSocket Manager]
B --> C[Connection Registry]
B --> D[Message Handler]
D --> E[Signature Verifier]
F[Client] <--> B
G[Controllers] --> H[SigningService]
H --> B
Key Components:
-
WebSocket 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
-
Signing Service
- Provides a clean API for controllers to use
- Abstracts WebSocket complexity from business logic
- Handles error cases and timeouts
3. Directory Structure
src/
├── websocket/
│ ├── mod.rs # Module 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 for controllers
├── controllers/
│ └── [existing controllers]
│ └── websocket.rs # WebSocket controller (if needed)
└── routes/
└── mod.rs # Updated to include WebSocket routes
4. Data Flow
sequenceDiagram
participant Client
participant WebSocketManager
participant Registry
participant Controller
participant SigningService
Client->>WebSocketManager: Connect
Client->>WebSocketManager: Introduce(public_key)
WebSocketManager->>Registry: Register(connection, public_key)
Controller->>SigningService: send_to_sign(public_key, message)
SigningService->>Registry: Lookup(public_key)
Registry-->>SigningService: connection
SigningService->>WebSocketManager: Send message to connection
WebSocketManager->>Client: Message to sign
Client->>WebSocketManager: Signed response
WebSocketManager->>SigningService: Forward response
SigningService->>SigningService: Verify signature
SigningService-->>Controller: Return verified response
5. Message Protocol
We'll define a simple JSON-based protocol for communication:
// Client introduction
{
"type": "introduction",
"public_key": "base64_encoded_public_key"
}
// Sign request
{
"type": "sign_request",
"message": "base64_encoded_message",
"request_id": "unique_request_id"
}
// Sign response
{
"type": "sign_response",
"request_id": "unique_request_id",
"message": "base64_encoded_message",
"signature": "base64_encoded_signature"
}
6. Required Dependencies
We'll need to add the following dependencies to the project:
# WebSocket support
actix-web-actors = "4.2.0"
# Cryptography
ed25519-dalek = "2.0.0" # For Ed25519 signatures
base64 = "0.21.0" # For encoding/decoding
rand = "0.8.5" # For generating random data
7. Implementation Details
7.1 WebSocket Manager
The WebSocket Manager will handle the lifecycle of WebSocket connections:
pub struct WebSocketManager {
registry: Arc<RwLock<ConnectionRegistry>>,
}
impl Actor for WebSocketManager {
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 WebSocketManager {
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<WebSocketManager>>,
}
impl ConnectionRegistry {
pub fn register(&mut self, public_key: String, addr: Addr<WebSocketManager>) {
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<WebSocketManager>> {
self.connections.get(public_key)
}
}
7.3 Signing Service
The Signing Service will provide a clean API for controllers:
pub struct SigningService {
registry: Arc<RwLock<ConnectionRegistry>>,
}
impl SigningService {
pub async fn send_to_sign(&self, public_key: &str, message: &[u8])
-> Result<(Vec<u8>, Vec<u8>), SigningError> {
// 1. Find the connection for the public key
let connection = self.registry.read().await.get(public_key).cloned();
if let Some(conn) = connection {
// 2. Generate a unique request ID
let request_id = Uuid::new_v4().to_string();
// 3. Create a response channel
let (tx, rx) = oneshot::channel();
// 4. Register the response channel
self.pending_requests.write().await.insert(request_id.clone(), tx);
// 5. Send the message to the client
let sign_request = SignRequest {
request_id: request_id.clone(),
message: message.to_vec(),
};
conn.do_send(sign_request);
// 6. Wait for the response with a timeout
match tokio::time::timeout(Duration::from_secs(60), rx).await {
Ok(Ok(response)) => {
// 7. Verify the signature
if self.verify_signature(&response.signature, message, public_key) {
Ok((response.message, response.signature))
} else {
Err(SigningError::InvalidSignature)
}
},
Ok(Err(_)) => Err(SigningError::ChannelClosed),
Err(_) => Err(SigningError::Timeout),
}
} else {
Err(SigningError::ConnectionNotFound)
}
}
}
8. Error Handling
We'll define a comprehensive error type for the signing service:
#[derive(Debug, thiserror::Error)]
pub enum SigningError {
#[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("WebSocket error: {0}")]
WebSocketError(#[from] ws::ProtocolError),
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
}
9. Security Considerations
- Public Key Validation: Validate public keys upon connection to ensure they are properly formatted
- 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
10. 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
11. Integration with Existing Controllers
Controllers can use the SigningService through dependency injection:
pub struct SomeController {
signing_service: Arc<SigningService>,
}
impl SomeController {
pub async fn some_action(&self, public_key: String, message: Vec<u8>) -> HttpResponse {
match self.signing_service.send_to_sign(&public_key, &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(),
}))
}
}
}
}
12. Deployment Considerations
- Scalability: The WebSocket 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