Skip to main content

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:

  1. 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}
  2. 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
  3. 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?:

  1. SPAKE2 shared secret: Long-lived credential, used for device pairing
  2. 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:

  1. Sender generates ephemeral X25519 keypair
  2. Sender derives ECDH shared secret with receiver's public key
  3. Shared secret → SHA-256 → AES-256 encryption key
  4. Plaintext encrypted with AES-256-GCM (96-bit nonce, 128-bit tag)
  5. Ciphertext + nonce + ephemeral public key sent to receiver

Decryption Process:

  1. Receiver extracts ephemeral public key from message
  2. Derives ECDH shared secret with own ephemeral private key
  3. Shared secret → SHA-256 → AES-256 decryption key
  4. Verifies GCM authentication tag
  5. 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 encryption
  • x25519-dalek: X25519 ECDH key exchange
  • ed25519-dalek: Ed25519 signatures (via SPAKE2)
  • sha2: SHA-256 hashing
  • spake2: 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

  1. Verify Pairing PIN: Always confirm PIN matches on both devices
  2. Secure Your Devices: Use full-disk encryption, strong passwords
  3. Install from Trusted Sources: Only download official releases
  4. Keep Updated: Apply security updates promptly
  5. Physical Security: Don't leave devices unattended while paired

For Developers

  1. Review Crypto Code: Changes to crypto should be peer-reviewed
  2. Pin Dependencies: Use Cargo.lock to ensure reproducible builds
  3. Audit Dependencies: Regularly check for CVEs in dependencies
  4. Minimize Secrets in Memory: Clear sensitive data after use
  5. Test Crypto: Unit tests for all cryptographic operations

For Operators (Server)

  1. Use TLS: Always use HTTPS with valid certificates
  2. Rate Limiting: Implement rate limiting on pairing endpoints
  3. Monitoring: Log suspicious activity (excessive pairing attempts)
  4. Message TTL: Enforce server-side message expiration (5 minutes)
  5. 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 ReadWritePaths below)

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
  • warn level for dependencies minimizes noise
  • info level for fido_bridge captures important events

Levels:

  • warn: Only warnings and errors from dependencies
  • fido_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, SystemCallFilter for defense-in-depth

Conclusion

FIDO Bridge's security design prioritizes:

  1. End-to-end encryption (server never sees plaintext)
  2. Forward secrecy (ephemeral key rotation)
  3. Mutual authentication (SPAKE2 pairing)
  4. 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