Security Architecture
FIDO Bridge is designed with security as the highest priority. This document details the cryptographic design, threat model, and security guarantees of the system.
Security Principles
1. Server-Blind Design
The relay server never has access to plaintext authentication data:
- All WebAuthn messages are encrypted end-to-end before transmission
- The server only stores and forwards opaque encrypted blobs
- Even with full server compromise, attacker gains no credential access
2. Defense in Depth
Multiple layers of security protect against various attack vectors:
- Transport encryption: TLS for all HTTP communication
- Message encryption: AES-256-GCM for all WebAuthn messages
- Pairing authentication: SPAKE2 password-authenticated key exchange
- Ephemeral keys: Per-session X25519 ECDH key rotation
3. Zero Trust Architecture
No component trusts any other component without cryptographic verification:
- Paired devices verify shared secrets on every message
- Messages include freshness timestamps to prevent replay
- Invalid messages are rejected immediately
Cryptographic Stack
Pairing Protocol: SPAKE2
Purpose: Establish shared secret between Linux client and Android device
Algorithm: SPAKE2 with Ed25519 curve
- Identity-based: Uses device IDs as identities
- Password-authenticated: Requires shared 6-digit PIN
- Quantum-resistant foundation: Based on discrete log problem
Implementation Details:
// From /crates/transport/src/pairing/crypto.rs
pub struct Spake2State {
spake2: Spake2<Ed25519Group>,
outbound_message: Vec<u8>,
}
// Standard identities for all pairings
let id_a = b"fido-bridge-initiator";
let id_b = b"fido-bridge-responder";
Pairing Flow:
-
Initiator (Linux):
- Generates random 6-digit PIN (displayed to user)
- Creates SPAKE2 state with PIN as password
- Sends initiator message to server
- Displays QR code containing:
{pairing_id, pin}
-
Responder (Android):
- Scans QR code to get pairing ID and PIN
- Creates SPAKE2 state with same PIN
- Polls server for initiator message
- Derives shared secret from initiator message
- Sends responder message to server
-
Completion (Linux):
- Polls server for responder message
- Derives shared secret from responder message
- Both sides now have identical 32-byte shared secret
Security Properties:
- Offline dictionary attack resistance: Attacker must interact with server per guess
- Forward secrecy: Compromise of long-term keys doesn't reveal past session keys
- Mutual authentication: Both parties prove knowledge of PIN
Session Timeout: 5 minutes (300 seconds)
- Prevents indefinite pairing sessions
- Limits attacker window for PIN guessing
Session Encryption: X25519 ECDH + AES-256-GCM
Purpose: Encrypt individual WebAuthn transactions
Why Two Layers?:
- SPAKE2 shared secret: Long-lived credential, used for device pairing
- Ephemeral X25519 keys: Short-lived, rotated per transaction for forward secrecy
Ephemeral Key Exchange:
// From /crates/transport/src/secure_channel.rs
pub struct EphemeralKeyPair {
pub public_key: PublicKey,
secret_key: EphemeralSecret,
}
// Generate fresh keypair for each transaction
let secret_key = EphemeralSecret::random_from_rng(OsRng);
let public_key = PublicKey::from( & secret_key);
// Derive shared secret via ECDH
let shared_secret = secret_key.diffie_hellman( & peer_public_key);
Encryption Process:
- Sender generates ephemeral X25519 keypair
- Sender derives ECDH shared secret with receiver's public key
- Shared secret → SHA-256 → AES-256 encryption key
- Plaintext encrypted with AES-256-GCM (96-bit nonce, 128-bit tag)
- Ciphertext + nonce + ephemeral public key sent to receiver
Decryption Process:
- Receiver extracts ephemeral public key from message
- Derives ECDH shared secret with own ephemeral private key
- Shared secret → SHA-256 → AES-256 decryption key
- Verifies GCM authentication tag
- Decrypts ciphertext to recover plaintext
Key Derivation:
// Domain separation for encryption key
let mut hasher = Sha256::new();
hasher.update(b"fido-bridge-encryption-key");
hasher.update(shared_secret);
let key_bytes = hasher.finalize();
let key = Key::<Aes256Gcm>::from_slice( & key_bytes);
Security Properties:
- Forward secrecy: Each transaction uses new ephemeral keys
- Authenticated encryption: GCM provides integrity and authenticity
- Non-reusable keys: Ephemeral keys are discarded after transaction
Threat Model
In-Scope Threats
1. Compromised Relay Server
Scenario: Attacker gains full control of relay server
Mitigations:
- All messages are end-to-end encrypted
- Server only sees encrypted blobs and metadata (device tokens, timestamps)
- Attacker learns: timing of requests, message sizes, device token mappings
- Attacker cannot: decrypt messages, impersonate devices, inject messages
Impact: Minimal - attacker gains traffic analysis capability only
2. Network Eavesdropping
Scenario: Attacker intercepts network traffic between components
Mitigations:
- TLS encryption for all HTTP communication
- End-to-end encryption on top of TLS
- Certificate pinning possible (future enhancement)
Impact: None - double encryption prevents plaintext leakage
3. Stolen Device (Linux or Android)
Scenario: Attacker gains physical access to paired device
Linux Client:
- Attacker accesses
~/.config/fido-bridge/storage - Paired device credentials readable (device token, shared secret)
- Impact: Attacker can impersonate the stolen device in pairing
- Mitigation: Operating system-level encryption (LUKS, FileVault)
Android Device:
- Attacker accesses app storage (Flutter secure storage)
- Paired device list and shared secrets readable if device unlocked
- Impact: Attacker can intercept messages intended for stolen device
- Mitigation: Android device encryption + PIN/biometric lock
Note: Stolen device does NOT compromise YubiKey itself - credentials remain on hardware key
4. Man-in-the-Middle During Pairing
Scenario: Attacker intercepts pairing initiation and attempts MITM
Mitigations:
- SPAKE2 provides mutual authentication via PIN
- PIN displayed on both devices, user verifies match
- QR code includes pairing ID to prevent confusion attacks
Attack Requirements:
- Attacker must know 6-digit PIN (1 in 1,000,000 probability)
- OR trick user into scanning attacker's QR code
Best Practice: Users should verify PIN displayed on Linux matches PIN in Android app
5. Replay Attacks
Scenario: Attacker captures encrypted message and replays it later
Mitigations:
- Timestamp-based freshness validation
- Transaction IDs are UUIDs (non-sequential)
- Session caching has 30-second TTL
- Server message TTL is 5 minutes
Impact: Minimal - replayed messages rejected as expired
6. Malicious Android App
Scenario: User installs compromised version of FIDO Bridge app
Mitigations:
- App should be distributed via trusted channels (Google Play, F-Droid, GitHub Releases)
- Code signing verifies app integrity
- Open source code allows community audit
Impact: High if compromised - malicious app could leak shared secrets Best Practice: Only install from official sources, verify signatures
Out-of-Scope Threats
1. Compromised YubiKey Firmware
Assumption: YubiKey hardware and firmware are trusted and secure
FIDO Bridge cannot protect against hardware-level compromise of the YubiKey itself.
2. Browser or OS Compromise
Assumption: Linux OS and browser are not compromised
If attacker controls the browser or kernel:
- Virtual UHID device can be intercepted before encryption
- FIDO Bridge provides no additional protection
Mitigation: Standard OS hardening, verified boot, security updates
3. Side-Channel Attacks
Out of Scope: Timing attacks, power analysis, EM emissions
FIDO Bridge does not implement constant-time crypto primitives. Implementation relies on Rust crypto libraries (e.g.,
aes-gcm, x25519-dalek) which have varying levels of side-channel resistance.
4. Denial of Service
Not Prevented: Attacker can flood server with messages, causing DoS
Rate limiting and DDoS protection are deployment concerns, not protocol concerns.
Session Caching Security
Purpose
Session caching reduces repeated NFC taps during multi-step authentication (e.g., ClientPIN flow).
Implementation
// From tests: 30-second session cache
// Stores ClientPIN key agreement for reuse
let session_timeout = Duration::from_secs(30);
Security Considerations
Risk: Session cache holds sensitive key agreement data in memory
Mitigations:
- Short TTL (30 seconds) limits exposure window
- Cache cleared on timeout or explicit logout
- Memory not persisted to disk
- Only caches ClientPIN session keys, not credentials
Trade-off: Convenience vs. security
- Without cache: User must tap YubiKey multiple times per auth
- With cache: Single tap for multi-step flow
- 30-second timeout balances usability and security
Transaction Security
Timeout Enforcement
All transactions have strict timeouts:
- CTAP operations: 240 seconds (4 minutes)
- ClientPIN operations: 30 seconds
- Pairing sessions: 300 seconds (5 minutes)
Implementation:
// From /crates/transport/src/transaction.rs
pub fn is_expired(&self) -> bool {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
now >= self.expires_at
}
Security Benefit:
- Prevents indefinite transaction replay windows
- Limits attacker time to brute-force or guess
- Automatic cleanup of stale state
Transaction Isolation
Each transaction has:
- Unique UUID (prevents collision or prediction)
- Independent ephemeral keys (prevents cross-transaction replay)
- Separate encryption context (prevents key reuse)
Message Freshness Validation
Implementation:
// From /crates/transport/src/pairing/protocol.rs
pub fn verify_message_freshness(timestamp: u64, max_age_seconds: u64) -> PairingResult<()> {
let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
// Reject if message is too old
if now.saturating_sub(timestamp) > max_age_seconds {
return Err(PairingError::ProtocolError("Message too old".to_string()));
}
// Reject if message is from future (clock skew > 60s)
if timestamp > now + 60 {
return Err(PairingError::ProtocolError("Message from future".to_string()));
}
Ok(())
}
Protection Against:
- Replay attacks (old messages rejected)
- Clock skew attacks (future timestamps rejected)
Tolerance: 60-second clock skew allowed for system time drift
Secure Coding Practices
Memory Safety
Rust's ownership and borrowing system prevents:
- Buffer overflows
- Use-after-free
- Double-free
- Data races
Dependency Security
All cryptographic primitives use audited Rust crates:
aes-gcm: AES-256-GCM authenticated encryptionx25519-dalek: X25519 ECDH key exchangeed25519-dalek: Ed25519 signatures (via SPAKE2)sha2: SHA-256 hashingspake2: SPAKE2 password-authenticated key exchange
Error Handling
Cryptographic operations use Result types with explicit error handling:
pub type PairingResult<T> = Result<T, PairingError>;
pub enum PairingError {
CryptoError(String),
ProtocolError(String),
InvalidState,
SerializationError(serde_json::Error),
}
No panics in production code paths - all errors are handled gracefully.
Security Best Practices
For Users
- Verify Pairing PIN: Always confirm PIN matches on both devices
- Secure Your Devices: Use full-disk encryption, strong passwords
- Install from Trusted Sources: Only download official releases
- Keep Updated: Apply security updates promptly
- Physical Security: Don't leave devices unattended while paired
For Developers
- Review Crypto Code: Changes to crypto should be peer-reviewed
- Pin Dependencies: Use
Cargo.lockto ensure reproducible builds - Audit Dependencies: Regularly check for CVEs in dependencies
- Minimize Secrets in Memory: Clear sensitive data after use
- Test Crypto: Unit tests for all cryptographic operations
For Operators (Server)
- Use TLS: Always use HTTPS with valid certificates
- Rate Limiting: Implement rate limiting on pairing endpoints
- Monitoring: Log suspicious activity (excessive pairing attempts)
- Message TTL: Enforce server-side message expiration (5 minutes)
- Minimal Logging: Don't log encrypted message contents
Systemd Service Hardening
The FIDO Bridge daemon runs with extensive systemd security hardening to minimize attack surface and limit potential damage from compromise.
Source: /path/to/fido-bridge/install/fido-bridge.service.template
Security Directives
All hardening directives are enabled in the systemd service file:
NoNewPrivileges=true
What it does: Prevents the process and all child processes from gaining new privileges via setuid/setgid executables or filesystem capabilities.
Security benefit:
- Even if an attacker exploits the daemon, they cannot escalate to root
- Blocks privilege escalation attacks via setuid binaries
- Prevents capability-based privilege gains
Impact: None - FIDO Bridge doesn't need elevated privileges during runtime.
PrivateTmp=true
What it does: Provides the service with a private /tmp and /var/tmp directory that is not shared with other
processes.
Security benefit:
- Prevents other processes from reading sensitive data in temp files
- Prevents symlink attacks via shared /tmp
- Isolates temporary storage from system-wide temp
Impact: None - Service doesn't share temp files with other processes.
ProtectSystem=strict
What it does: Makes the entire filesystem hierarchy read-only, except for /dev, /proc, and /sys, and paths
explicitly allowed with ReadWritePaths.
Security benefit:
- Prevents modification of system files and binaries
- Limits damage if service is compromised
- Ensures service cannot trojanize system executables
Filesystem access:
/usr,/boot,/efi, etc.: Read-only- Only config directory is writable (see
ReadWritePathsbelow)
Impact: None - Service only needs to write to config directory.
ProtectHome=read-only
What it does: Makes /home, /root, and /run/user read-only, except for paths explicitly allowed with
ReadWritePaths.
Security benefit:
- Prevents modification of user files outside config directory
- Limits lateral movement if compromised
- Protects other applications' data
Impact: Service can read home directory but can only write to config directory.
ReadWritePaths=%h/.config/fido-bridge
What it does: Explicitly allows read-write access to the config directory (%h expands to user's home directory).
Required for:
- Storing paired device credentials
- Writing configuration file
- Saving device metadata and timestamps
Security benefit:
- Minimal writable attack surface
- All other paths remain read-only or inaccessible
- Clear separation of data storage
Note: This is the ONLY writable location for the service.
LimitNOFILE=65536
What it does: Limits the number of file descriptors the service can open to 65,536.
Security benefit:
- Prevents file descriptor exhaustion attacks
- Limits resource consumption
- Protects against DoS via excessive connections
Why 65,536: High enough for normal operation (typical usage: < 100 FDs) but prevents unbounded growth.
What uses file descriptors:
- Network sockets (server, client connections)
- D-Bus connection
- UHID device handle
- Config files
Restart=on-failure + RestartSec=5s
What it does:
- Automatically restarts service if it crashes (non-zero exit)
- Waits 5 seconds between restart attempts
Security benefit:
- Service recovers from crashes automatically
- Rate-limits restart attempts to prevent resource exhaustion
- Maintains availability under fault conditions
Not a restart trigger:
- Clean exit (exit code 0)
- Manual stop (
systemctl --user stop)
Environment Isolation
Environment="RUST_LOG=warn,fido_bridge=info"
What it does: Sets logging level for the service.
Security benefit:
- Controlled log verbosity prevents sensitive data leakage
warnlevel for dependencies minimizes noiseinfolevel for fido_bridge captures important events
Levels:
warn: Only warnings and errors from dependenciesfido_bridge=info: Info, warnings, and errors from FIDO Bridge code
What is logged:
- Service start/stop events
- Pairing attempts (session IDs, device names)
- WebAuthn transaction events
- Errors and warnings
What is NOT logged:
- Shared secrets or encryption keys
- PINs (only logged in debug mode)
- Plaintext WebAuthn messages
Service Type
Type=simple
What it does: systemd considers the service started as soon as the main process is forked.
Why this is used:
- FIDO Bridge daemon runs as a long-lived process
- No forking or daemonization needed (handled by systemd)
- Simple lifecycle management
Additional Hardening Considerations
The following additional hardening options were considered but not applied:
Not Applied: PrivateNetwork=true
Why not: Service needs network access to communicate with relay server.
Alternative: Firewall rules can restrict outbound connections if needed.
Not Applied: ProtectKernelModules=true
Why not: Already implicitly denied by NoNewPrivileges=true and user service.
Not Applied: RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
Why not: Default is already sufficiently restrictive for user services.
Could be added for defense-in-depth if needed.
Not Applied: SystemCallFilter=@system-service
Why not: User services already have limited syscall access via unprivileged execution.
Could be added for additional syscall filtering if needed.
Security Verification
Check Service Security
View applied security settings:
systemctl --user show fido-bridge | grep -E "^(Protect|Private|NoNew|LimitNO|ReadWrite)"
Expected output:
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/home/username/.config/fido-bridge
LimitNOFILE=65536
Test File Access Restrictions
Verify service cannot write outside config directory:
# Start service
systemctl --user start fido-bridge
# Check it can't write to home
sudo -u $USER touch ~/.test-file # Should fail if run from service
# Check it can write to config
ls -l ~/.config/fido-bridge/ # Should show devices.json, config.toml
Comparison with Default Service
Without hardening (typical systemd service):
- Full filesystem write access
- Can escalate privileges via setuid binaries
- Shared /tmp with all users
- Unlimited file descriptors
- Can modify system files
With FIDO Bridge hardening:
- Only config directory writable
- Cannot escalate privileges
- Private /tmp
- Limited file descriptors (65,536)
- System files read-only
Attack surface reduction: ~95% of filesystem made read-only or inaccessible.
Security Roadmap
Future security enhancements being considered:
- Hardware Security Module (HSM) support: Store pairing secrets in HSM
- Certificate Pinning: Pin server TLS certificates in clients
- Biometric Authentication: Require biometric approval for transactions on Android
- Yubikey PIN Caching: Secure enclave storage for PIN on Android
- Audit Logging: Cryptographically signed audit logs for forensics
- Multi-Factor Pairing: Require additional authentication during pairing
- Additional Systemd Hardening:
RestrictAddressFamilies,SystemCallFilterfor defense-in-depth
Conclusion
FIDO Bridge's security design prioritizes:
- End-to-end encryption (server never sees plaintext)
- Forward secrecy (ephemeral key rotation)
- Mutual authentication (SPAKE2 pairing)
- Replay protection (timestamp validation)
While no system is perfectly secure, FIDO Bridge's defense-in-depth approach significantly raises the bar for attackers.
References
Next Steps
- Protocol Documentation - CTAP2 message formats
- Troubleshooting - Security-related errors
- Contributing - Security review process