db/heromodels/docs/sigsocket_architecture.md
2025-06-27 12:11:04 +03:00

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:

  1. SigSocket Manager

    • Handles WebSocket connections
    • Manages connection lifecycle
    • Routes messages to appropriate handlers
  2. Connection Registry

    • Maps public keys to active WebSocket connections
    • Handles connection tracking and cleanup
    • Provides lookup functionality
  3. Message Handler

    • Processes incoming messages
    • Implements the message protocol
    • Manages timeouts for responses
  4. Signature Verifier

    • Verifies signatures using public keys
    • Implements cryptographic operations
    • Ensures security of the signing process
  5. 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:

  1. Decodes the hex-encoded public key and signature
  2. Hashes the message using SHA-256 (required for secp256k1)
  3. Verifies the signature using the secp256k1 library
  4. Returns a boolean indicating whether the signature is valid

10. Security Considerations

  1. Public Key Validation: Validate public keys upon connection to ensure they are properly formatted secp256k1 keys
  2. Message Authentication: Consider adding a nonce or timestamp to prevent replay attacks
  3. Rate Limiting: Implement rate limiting to prevent DoS attacks
  4. Connection Timeouts: Automatically close inactive connections
  5. Error Logging: Log errors but avoid exposing sensitive information
  6. Input Validation: Validate all inputs to prevent injection attacks
  7. Secure Hashing: Always hash messages before signing to prevent length extension attacks

11. Testing Strategy

  1. Unit Tests: Test individual components in isolation
  2. Integration Tests: Test the interaction between components
  3. End-to-End Tests: Test the complete flow from client connection to signature verification
  4. Load Tests: Test the system under high load to ensure stability
  5. 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

  1. Scalability: The SigSocket server should be designed to scale horizontally
  2. Monitoring: Implement metrics for connection count, message throughput, and error rates
  3. Logging: Log important events for debugging and auditing
  4. Documentation: Document the API and protocol for client implementers