Architecture Overview
FIDO Bridge is a multi-component system designed to bridge WebAuthn/FIDO2 authentication from Linux browsers to NFC-capable Android devices. This document provides a comprehensive overview of the system architecture.
System Architecture
Component Breakdown
1. Linux Rust Client (/crates/client)
The Linux client is the core component running on the desktop machine.
Key Responsibilities:
- Creates and manages a virtual UHID (USB HID) FIDO device at
/dev/hidrawX - Implements the CTAP2 (Client to Authenticator Protocol 2) specification
- Handles WebAuthn requests from browsers (GetInfo, MakeCredential, GetAssertion)
- Manages end-to-end encryption with paired Android devices
- Maintains transaction state and timeouts
- Stores paired device credentials in
~/.config/fido-bridge/
Key Files:
src/uhid_fido.rs: UHID device management and CTAP2 protocol handlersrc/webauthn_handler.rs: WebAuthn message parsing and response constructionsrc/message_handler.rs: Message routing and transaction managementsrc/storage.rs: Persistent storage for paired devicessrc/uhid.rs: Low-level UHID kernel interface
Technical Details:
- Uses Linux UHID (User-space HID) subsystem for creating virtual FIDO devices
- Spawns async tasks for message polling (250ms interval during active transactions)
- Implements timeout handling (240s for CTAP operations, 30s for ClientPIN)
- Supports multiple concurrent transactions with unique transaction IDs
2. Relay Server (/crates/server)
A lightweight, stateless relay server that forwards encrypted messages between clients and Android devices.
Key Responsibilities:
- Accepts and queues encrypted messages from Linux clients
- Provides polling endpoint for Android devices
- Maintains temporary message storage with 5-minute TTL
- Handles SPAKE2 pairing protocol messages
- Implements CORS for web-based administration (optional)
Key Files:
src/main.rs: Axum web server setup and routingsrc/blob.rs: Message blob storage with expirationsrc/handlers.rs: HTTP endpoint handlers
Design Principles:
- Server-blind: Never decrypts messages, only forwards encrypted blobs
- Stateless: No persistent storage of credentials or keys
- Minimal: Simple queue-based architecture with TTL cleanup
Endpoints:
POST /api/pairing/initiate - Start pairing session
GET /api/pairing/poll/:id - Poll for pairing responses
POST /api/messages/send - Send encrypted message
GET /api/messages/poll/:token - Poll for messages by device token
3. Transport Library (/crates/transport)
Shared library containing cryptographic primitives, protocol definitions, and message types.
Key Responsibilities:
- SPAKE2 pairing protocol implementation
- X25519 ECDH ephemeral key exchange
- AES-256-GCM encryption/decryption
- Message serialization/deserialization
- Transaction state management
Key Files:
src/pairing/protocol.rs: SPAKE2 pairing flow and device infosrc/pairing/crypto.rs: Cryptographic operations (SPAKE2, Ed25519)src/secure_channel.rs: Ephemeral key exchange and AES-GCM encryptionsrc/webauthn_messages.rs: CTAP2 message types (GetInfo, MakeCredential, etc.)src/transaction.rs: Transaction lifecycle and state machinesrc/client.rs: HTTP client for server communication
Cryptographic Stack:
- Pairing: SPAKE2 with Ed25519 curve
- Session Encryption: X25519 ECDH + AES-256-GCM
- Key Derivation: SHA-256 for deriving encryption keys
- Random:
OsRngfor cryptographically secure randomness
4. Flutter Android App (/app)
The Android application provides the NFC interface and user interaction.
Key Responsibilities:
- NFC communication with YubiKey via APDU commands
- Message polling from relay server (adaptive intervals)
- Pairing UI with QR code scanning and PIN entry
- Transaction approval/rejection UI
- Background service for persistent message monitoring
- Session caching to reduce NFC taps
Key Files:
lib/main.dart: Application entry point and notification setuplib/src/screens/home_screen.dart: Main UI with transaction listlib/src/services/background_service.dart: Background polling servicelib/src/services/nfc_service.dart: NFC APDU communicationlib/src/providers/pairing_provider.dart: Pairing state managementlib/rust/api/simple.dart: Flutter Rust Bridge bindings
Kotlin NFC Layer:
android/app/src/main/kotlin/com/example/fido_bridge/NfcHandler.kt: Low-level NFC tag handling- Implements ISO 14443A protocol for YubiKey communication
- Handles APDU command/response parsing
Polling Strategy:
- Active polling: 250ms interval when waiting for responses
- Background polling: Reduced frequency when app is backgrounded
- Exponential backoff on errors
5. Data Flow
Pairing Flow
WebAuthn Authentication Flow
Storage and State Management
Linux Client Storage
Location: ~/.config/fido-bridge/
Stored Data:
- Paired device credentials (device ID, device token, shared secret)
- Device metadata (name, type, OS version)
- Pairing timestamps
Format: JSON serialization via serde_json
Android App Storage
Uses Flutter Riverpod for state management with persistent storage.
Stored Data:
- Paired device list
- Device metadata
- Shared secrets (encrypted at rest)
- Last sync timestamps
Session Cache:
- ClientPIN session keys (30-second TTL)
- Recent transactions for quick retry
Configuration
Linux Client Configuration
File: ~/.config/fido-bridge/config.toml
[server]
url = "https://relay.fido-bridge.example.com"
mode = "remote" # or "embedded"
[logging]
level = "info" # debug, info, warn, error
Environment Variables
FIDO_BRIDGE_SERVER_URL: Override server URLFIDO_BRIDGE_LOG_LEVEL: Set logging verbosityFIDO_BRIDGE_STORAGE_PATH: Custom storage directory
Performance Characteristics
Latency
- Local encryption/decryption: Less than 5ms
- Network round-trip (relay server): 50-200ms (depends on network)
- NFC communication: 100-500ms (depends on operation)
- Total authentication time: 1-3 seconds
Polling Overhead
- Linux client: 250ms poll interval during active transactions (minimal CPU usage)
- Android app: 250ms active, reduced in background (battery-optimized)
Transaction Timeouts
- CTAP operations: 240 seconds (4 minutes)
- ClientPIN operations: 30 seconds
- Pairing session: 300 seconds (5 minutes)
- Server message TTL: 300 seconds (5 minutes)
Error Handling and Recovery
Transaction Expiration
Transactions automatically expire after timeout, with cleanup:
- Client removes transaction from active set
- Server purges expired messages
- Android app displays timeout error to user
Network Failures
- Exponential backoff on polling errors
- Automatic retry with jitter
- User notification of connection issues
NFC Communication Errors
- Automatic retry for transient errors
- User guidance for "tag lost" errors
- Session cache minimizes repeated taps
Pairing Failures
- Clear error messages (QR code parse errors, PIN mismatch, timeout)
- Automatic cleanup of incomplete pairing sessions
- Ability to retry pairing without restarting components
Daemon Component
The Linux client runs as a systemd user daemon, managing multiple background services and coordinating all client-side operations.
Source: /path/to/fido-bridge/crates/client/src/daemon.rs
Daemon Startup Sequence
When the daemon starts (lines 76-280), it performs these steps in order:
-
Log Startup Information (lines 77-80):
- Logs device ID
- Logs server URL
- Logs config file location
-
Setup Signal Handlers (lines 82-85):
- Registers handlers for SIGTERM, SIGINT, SIGHUP
- Spawns async task to listen for signals
- See "Signal Handling" section below
-
Start Embedded Server (optional, lines 88-97):
- Only if
config.server_modeisEmbedded - Starts HTTP server on configured port (default: 3000)
- Server runs in background as async task
- Only if
-
Start D-Bus Service (lines 99-101):
- Registers
org.fidobridge.Clienton session bus - Object path:
/org/fidobridge/Client - Provides all pairing and management methods
- See D-Bus API Documentation for details
- Registers
-
Start UHID FIDO Device (lines 104-245):
- Creates virtual FIDO device at
/dev/hidrawX - Spawns event loop for UHID device handling
- Spawns WebAuthn message sender task
- Routes requests to paired devices via relay server
- Handles device-specific routing to prevent broadcast issues (lines 140-171)
- Creates virtual FIDO device at
-
Start Background Message Polling (lines 247-261):
- Polls relay server every 250ms for incoming responses
- Processes WebAuthn responses from Android devices
- Routes responses to appropriate UHID channel
-
Wait for Signals (line 268):
- Main thread blocks waiting for termination signal
- All other work happens in background async tasks
Background Tasks
The daemon spawns several long-running async tasks:
1. Signal Handler Task
Source: Lines 16-30, spawned at line 85
What it does:
- Listens for POSIX signals (SIGTERM, SIGINT, SIGHUP)
- On SIGTERM/SIGINT: Logs shutdown message and exits cleanly
- On SIGHUP: Logs reload message (config reload TODO)
Signals handled:
Signals::new([SIGTERM, SIGINT, SIGHUP])
2. Embedded Server Task
Source: Lines 88-97
When started: Only if config.server_mode is ServerMode::Embedded { port }
What it does:
- Runs the relay server locally
- Listens on configured port
- Same functionality as standalone server
3. D-Bus Service Task
Source: Lines 99-101, registered via start_dbus_service()
What it does:
- Handles D-Bus method calls
- Emits signals for pairing events
- Manages pairing session cleanup (60-second interval)
- See
/path/to/fido-bridge/crates/client/src/dbus_interface.rs
Session Cleanup: Lines 520-563 in dbus_interface.rs
- Runs every 60 seconds
- Removes expired pairing sessions
- Emits
PairingFailedsignals for timeouts
4. UHID Device Event Loop Task
Source: Lines 113-117
What it does:
- Reads CTAP2 commands from UHID device
- Processes WebAuthn requests (GetInfo, MakeCredential, GetAssertion)
- Creates transactions with unique IDs
- Sends encrypted requests to relay server
- Maintains pending transaction state
Transaction management:
- Each request gets unique UUID
- Timeout: 240s for CTAP operations, 30s for ClientPIN
- Transactions stored in shared state for response routing
5. WebAuthn Message Sender Task
Source: Lines 123-235
What it does:
- Receives WebAuthn messages from UHID device
- Determines routing: broadcast vs. device-specific (lines 140-171)
- Loads paired device credentials from storage
- Encrypts messages with device-specific shared secrets
- Sends to relay server
Device-Specific Routing (lines 140-171):
- After initial GetInfo response, subsequent messages route only to responding device
- Prevents duplicate responses from multiple devices
- Maps UHID channels to device IDs
Broadcast vs. Targeted:
- Broadcast: Initial GetInfo request (discovery phase)
- Targeted: All subsequent requests in transaction
6. Message Polling Task
Source: Lines 247-261
What it does:
- Polls relay server every 250ms
- Calls
poll_messages_background()function (lines 32-74) - Retrieves encrypted responses from Android devices
- Decrypts responses using shared secrets
- Routes responses to UHID device via transaction ID mapping
Polling function (lines 32-74):
async fn poll_messages_background(
config: &ClientConfig,
pending_transactions: Option<Arc<Mutex<HashMap<...>>>>,
channel_device_map: Option<Arc<Mutex<HashMap<...>>>>,
) -> Result<()>
What it does:
- Creates client with device ID
- Loads all paired device shared secrets
- Calls
client.get_messages().await - Passes messages to
MessageHandler::process_messages() - Routes responses to correct UHID channel
Component Interactions
┌─────────────────────────────────────────────────────────┐
│ Main Thread │
│ - Waits for termination signal │
└────────────────────────┬────────────────────────────────┘
│
┌───────────────┼───────────────┐
│ │ │
┌────────▼────────┐ ┌───▼──────┐ ┌─────▼──────────┐
│ Signal Handler │ │ D-Bus │ │ UHID Device │
│ Task │ │ Service │ │ Event Loop │
│ - SIGTERM │ │ Task │ │ - Read CTAP2 │
│ - SIGINT │ │ - RPC │ │ - Create Txn │
│ - SIGHUP │ │ - Pair │ │ - Send Req │
└─────────────────┘ └──────────┘ └────┬───────────┘
│
┌─────────────────┴──────────┐
│ │
┌──────────▼────────┐ ┌───────────▼──────┐
│ WebAuthn Sender │ │ Message Poller │
│ Task │ │ Task │
│ - Encrypt │ │ - Poll Server │
│ - Route to Dev │ │ - Decrypt │
│ - Send to Server │ │ - Route to UHID │
└───────────────────┘ └──────────────────┘
Signal Handling
Source: Lines 16-30
Graceful Shutdown Sequence (triggered by SIGTERM/SIGINT):
-
Signal received (lines 19-22):
SIGTERM | SIGINT => {
info!("Received termination signal, shutting down gracefully...");
std::process::exit(0);
} -
Cleanup on shutdown (lines 270-278):
- Signal handle closed
- Embedded server stopped (if running)
- Shutdown logged
Note: Task cleanup is automatic via Rust's Drop trait and tokio's runtime shutdown.
Resource Management
Source: Service template at /path/to/fido-bridge/install/fido-bridge.service.template
Resource Limits:
LimitNOFILE=65536: File descriptor limit (for many simultaneous connections)PrivateTmp=true: Isolated /tmp directoryProtectSystem=strict: Read-only system directoriesProtectHome=read-only: Read-only home except config directoryReadWritePaths=%h/.config/fido-bridge: Only config dir is writable
Restart Policy:
Restart=on-failure: Auto-restart on crashesRestartSec=5s: Wait 5 seconds before restart
Security Hardening:
NoNewPrivileges=true: Prevents privilege escalation- See Security Documentation for more details
Configuration Reload
Current Status: Partially implemented
SIGHUP Handler (lines 23-26):
SIGHUP => {
info!("Received SIGHUP, reloading configuration...");
// TODO: Implement config reload
}
Planned Behavior:
- Reload
~/.config/fido-bridge/config.toml - Update log level
- Reconnect to server if URL changed
- No restart required
Security Considerations
See the Security Documentation for detailed analysis of the threat model, encryption layers, and attack surface.
Next Steps
- Security Architecture - Detailed security analysis
- D-Bus API Reference - D-Bus interface details
- Service Management - Managing the daemon
- Protocol Documentation - CTAP2 and WebAuthn message formats
- Building from Source - Development setup