Skip to main content

Known Limitations of Formal Verification

Time to read: 35 minutes
Prerequisites: Understanding of all formal verification docs
Difficulty: Advanced
Related: Signal Protocol Security Audit


The Simple Story

Formal verification is powerful but not magic.

What It Proves:

  • Protocol design is mathematically sound
  • No logical flaws in the model
  • No attacker can violate specified security properties

What It Does NOT Prove:

  • Implementation correctness
  • Side-channel resistance
  • Computational hardness assumptions
  • Real-world performance issues

This document explains these limitations clearly so you understand what's proven and what's NOT proven.


Mental Model: Proven vs. Assumed

Hold this picture in your head:


Limitation Summary

CategoryLimitationWhat It MeansHow to Address
ImplementationProVerif proves MODEL secure, not CODE correctBugs in code can happen even with secure protocolCode review + testing + fuzzing
Side-ChannelsAssumes no timing/cache variationsReal platforms have timing attacksTiming analysis + platform hardening
HardnessAssumes ECC, discrete log are hardIf these break, protocol breaksFollow crypto research + post-quantum crypto
PerformanceDoesn't model performance/memoryReal-world resource constraintsPerformance testing + load testing
Black-Box CryptographyAssumes crypto primitives workIf X25519 has bugs, flaws propagateUse audited crypto libraries
Network RealityModels network as black-boxReal-world: packet loss, DoS, etc.Network testing + resilience design
Human FactorsAssumes parties follow protocolUsers can be tricked, keys mishandledUser education + operational security
Quantum FutureNo quantum resistance provenQuantum computers could break ECCPlan for post-quantum migration
End-to-End CorrespondenceX3DH ↔ DR not linked in ProVerifsignal_protocol_complete.pv doesn't channel-linkSee implementation nuances
Double Ratchet F*F* verification resource-intensiveverify-lite skips DR; full verify needs 512MBUse make verify-lite for iteration

Limitation 1: Implementation Correctness

WARNING: Formal Verification ≠ Code Verification

What ProVerif Proves:

  • The mathematical MODEL of the protocol is secure
  • All attacker models cannot break the protocol

What It Does NOT Prove:

  • The IMPLEMENTATION matches the MODEL
  • The code has no bugs
  • The code is compiled correctly

Why This Matters

Example Scenarios:

Scenario A: Code Bug

  • ProVerif says: Protocol is secure
  • Implementation: Off-by-one error in array index
  • Result: Buffer overflow, memory corruption
  • Formal verification: Doesn't catch implementation bugs

Scenario B: Mismatch

  • ProVerif says: DH1 = DH(IK_A, SPK_B)
  • Implementation: Swaps to DH(SP_B, IK_A) due to copy-paste error
  • Result: Wrong DH computation
  • Formal verification: Doesn't verify code-model correspondence

The Solution

Formal Verification + Implementation Verification:

Best Practices:

  • Formal verification proves protocol design secure
  • Code review verifies implementation matches model
  • Testing confirms edge cases work
  • Fuzzing catches implementation bugs
  • Together: Protocol + Implementation both verified

Addressing This Limitation

We Addressed It:

  • Document 06 Implementation Alignment proves ProVerif ↔ Rust match
  • Line-by-line comparisons verify code-model correspondence
  • GitHub links provided for verification

You Should Also:

  • Add unit tests for all protocol operations
  • Add integration tests for X3DH and Double Ratchet
  • Use fuzzing tools (AFL, libFuzzer) on crypto code
  • Conduct code reviews before merging
  • Use Rust's memory safety to prevent buffer overflows

Limitation 2: Side-Channel Resistance

WARNING: ProVerif Doesn't Model Timing, Cache, Power

What ProVerif Assumes:

  • All operations take the same time
  • No cache-based information leakage
  • No power analysis possible
  • Cryptographic operations are black-boxes

Reality:

Timing Attacks:

// DANGEROUS: Not constant-time
fn compare_keys(a: &[u8], b: &[u8]) -> bool {
for i in 0..a.len() {
if a[i] != b[i] {
return false; // Early exit leaks info!
}
}
true
}

Result: Attacker measures time to learn where keys differ.

Constant-Time Version:

// SAFE: Constant-time comparison
fn compare_keys(a: &[u8], b: &[u8]) -> bool {
let mut result = 0u8;
for i in 0..a.len() {
result |= a[i] ^ b[i]; // Always processes all bytes
}
result == 0
}

ProVerif: Doesn't model timing difference between these two versions.

How to Address

Platform-Specific Hardening:

  • Use constant-time library functions (subtle crate in Rust)
  • Avoid early returns in key comparisons
  • Use hardware acceleration (AES-NI) when available
  • Compile with anti-timing-attack flags
  • Test timing variations manually

For This Project:

// Use the `subtle` crate for constant-time operations
use subtle::ConstantTimeEq;

impl ConstantTimeEq for [u8; 32] {
fn ct_eq(&self, other: &Self) -> subtle::Choice {
let mut result = subtle::Choice::from(1u8);
for i in 0..32 {
result &= self[i].ct_eq(&other[i]); // Constant-time
}
result
}
}

WARNING: JavaScript/WebAssembly Side-Channels

WASM in Browser:

  • JavaScript timing can leak information even in WASM
  • Browser cache can expose patterns
  • Resource contention (multitab browser) causes timing variations

Mitigation:

  • Minimize timing-critical operations in JavaScript
  • Add random delays to blur timing
  • Use browser security APIs (when available)

Limitation 3: Computational Hardness Assumptions

WARNING: ProVerif Assumes Crypto Primitives Are Unbreakable

ProVerif Assumptions:

  • DH operation is perfectly secure
  • Digital signatures are unforgeable
  • Symmetric encryption is indistinguishable
  • Hash functions are collision-resistant

If These Fail, All Proofs Fail:

Example: Quantum Computers

  • Proverif says: DH(EK_A, IK_B) is secure
  • Quantum: Shor's algorithm breaks ECC
  • Result: Signal Protocol compromised

Current Cryptographic Landscape

What We Assume:

  • X25519 ECDH has 128-bit security
  • Ed25519 signatures are unforgeable
  • AES-256-GCM is secure
  • SHA-256 is collision-resistant

Evidence:

  • Decades of research (2000s-2020s)
  • NIST/FIPS standardization
  • Extensive cryptanalysis
  • No practical breaks found

But NOT Guaranteed:

  • Future research could break these
  • Quantum computers (if practical) would break ECC
  • Side-channel attacks can break implementations

How to Address

Monitor Crypto Research:

  • Follow NIST/Cryptology news
  • Update cryptographic primitives if broken
  • Plan for post-quantum migration
  • Use hybrid approaches (classical + post-quantum)

For This Project:

  • Consider adding ML-KEM or other post-quantum KEM as an option
  • Follow CVEs for dependencies:
    • curve25519-dalek (ECC library)
    • ed25519-dalek (signatures)
    • aes-gcm (encryption)
    • sha2 (hashing)

Limitation 4: Black-Box Cryptography Model

WARNING: ProVerif Assumes Crypto Primitives Work Correctly

ProVerif Models:

fun dh(skey, pkey): bitstring.
fun sign(message, skey): signature.
fun encrypt(key, message): ciphertext.

let dh_out = dh(private_key, public_key) in
let sig = sign(message, private_key) in
let cipher = encrypt(key, message) in

ProVerif:

  • Treats dh(), sign(), encrypt() as perfect mathematical functions
  • No bugs in implementation (assumes)
  • No timing variations (assumes)
  • Implementation is unbreakable (assumes)

Reality:

  • If curve25519-dalek has a bug, the protocol fails
  • If ed25519-dalek has a timing leak, signatures are forgeable
  • If aes-gcm has side-channel vulnerability, encryption is weak

How to Address

Use Well-Audited Libraries:

  • curve25519-dalek (peer-reviewed Rust crate)
  • ed25519-dalek (peer-reviewed Rust crate)
  • aes-gcm (AES-NI hardware accelerated)
  • sha2 (well-studied hash function)

Monitor Vulnerabilities:

# Check for CVEs
cargo audit
cargo outdated

# Follow dependency updates
cargo update

Use Dependency Scanning:

  • GitHub Dependabot
  • RenovateBot
  • Cargo Audit
  • Snyk

Limitation 5: Network Reality Model

WARNING: ProVerif Doesn't Model Network Conditions

ProVerif Model:

free c: channel [private].

out(c, message). // Send
in(c, message). // Receive (instantly!)

Assumes:

  • Network delivers messages perfectly
  • No packet loss
  • No reordering
  • No delay variations
  • No DoS attacks

Reality:

  • Packet loss causes message delivery failures
  • Reordering makes message sequencing tricky
  • Delays cause timeout errors
  • DoS attacks prevent communication
  • Network partitions break the protocol

How This Affects Security

DoS Vulnerability:

ProVerif says: Attacker cannot learn secrets 
But if attacker can:
- Block network traffic (DoS)
- Exhaust resources (message flood)
- Cause timeouts
Result: Communication disrupted

Protocol is secure, but unavailable

Remediation:

  • Add rate limiting (prevent message flooding)
  • Add retry logic (handle packet loss)
  • Add timeout handling (resilience)
  • Add DoS detection and blocking

Example: Flood Attack

Scenario:

// VULNERABLE: No rate limiting
async fn handle_message(message: Message) {
// Process EVERY message
let decrypted = decrypt(&message)?;
process(decrypted)?;
}

// DoS: Send 10,000 messages per second → Server crashes

Fixed:

// SECURE: With rate limiting
async fn handle_message(message: Message) {
if rate_limiter.check_limit() {
return Err("Too many messages");
}

let decrypted = decrypt(&message)?;
process(decrypted)?;
}

Formal verification: Doesn't model DoS resilience (implementation requirement).


Limitation 6: Human Factors

WARNING: ProVerif Assumes Parties Follow Protocol

ProVerif Models:

  • Alice follows the protocol correctly
  • Bob follows the protocol correctly
  • No one makes mistakes

Reality:

User Mistakes:

  • User copies private keys unencrypted
  • User shares public keys over untrusted channels
  • User doesn't verify signatures
  • User stores secrets in weak passwords

Example:

Correct Use:

// Store encrypted
let encrypted = encrypt_aes_gcm_key_derivation(user_password, private_key)?;
fs::write("private.key.enc", encrypted)?;

Mistake:

// Store plaintext!
fs::write("private.key", private_key)?; // DANGEROUS

Formal verification: Doesn't model user behavior, only protocol correctness.

How to Address

User Education:

  • Don't share private keys
  • Use strong passwords
  • Verify peer identities
  • Keep software updated

Built-in Protection:

  • Enforce key encryption at rest
  • Verify channel integrity
  • Warn before sending unencrypted
  • Auto-delete sensitive data

Limitation 7: Quantitative Performance

WARNING: ProVerif Doesn't Measure Performance

ProVerif Model:

  • Operations take one step (abstract)
  • No memory usage
  • No time limits
  • No resource constraints

Reality:

Performance Problems:

ProVerif says: Derive 10,000 keys 
Reality: Takes 5 seconds, 100 MB memory

ProVerif says: X3DH works
Reality: Takes 50 ms, 2 MB memory

For Real-World Use:

  • Mobile devices have limited memory
  • Browser WASM has performance limits
  • Network bandwidth affects responsiveness

How to Address

Performance Testing:

#[bench]
fn bench_x3dh_handshake() {
let start = Instant::now();
x3dh_handshake(alice_keys, bob_keys)?;
let elapsed = start.elapsed();
assert!(elapsed < Duration::from_millis(50)); // < 50ms

// Memory usage: < 1MB
let memory = get_memory_usage();
assert!(memory < 1024 * 1024);
}

Monitoring:

  • Track message encryption time
  • Track memory usage
  • Track network latency impact
  • Set performance thresholds

Critical Limitation: No End-to-End Channel Linking

IMPORTANT: signal_protocol_complete.pv Limitation

From 03 Double Ratchet Formal Proof:

"Note: The signal_protocol_complete.pv model doesn't link the X3DH handshake to the Double Ratchet phase with a channel, so it doesn't prove the full end-to-end correspondence."

What This Means:

What ProVerif Proves:

  • X3DH handshake is secure (separate model)
  • Double Ratchet messaging is secure (separate model)

What ProVerif Does NOT Prove:

  • X3DH handshake leads to correct Double Ratchet initialization
  • End-to-end message delivery from Alice → Bob

Why This Is Important:

Formal verification models X3DH and Double Ratchet separately. The complete model doesn't link them via a channel (e.g., in(c, handshake) → out(c, double_ratchet_start)). Therefore:

  • Cannot prove: "If Alice does X3DH, then Double Ratchet starts with the correct root key"
  • Cannot prove: "If handshake fails, Double Ratchet doesn't start"

What This Means:

  • Individual phases are formally verified
  • End-to-end connection relies on implementation correctness
  • Integration testing is required

Quick Check: What ProVerif Proves vs. What It Doesn't

ProVerif Proves:

Protocol design is mathematically sound

X3DH handshake is secure Double Ratchet provides forward secrecy No logical flaws in protocol Attacker cannot break the model

ProVerif Does NOT Prove:

Everything else: implementation, side-channels, hardness, etc.

ProVerif does NOT prove:

  • Implementation is correct (code bugs possible)
  • Side-channel resistance (timing attacks possible)
  • Cryptographic primitives are secure (if library has bugs)
  • Network resilience (packet loss, DoS)
  • User behavior follows protocol (human mistakes)
  • Performance requirements (speed, memory)
  • Quantum resistance (future computers)
  • End-to-end correspondence (X3DH → Double Ratchet link)

Key Takeaways

Proven: Protocol design is mathematically secure
Proven: 7 security properties hold for all attackers
Proven: ProVerif ↔ Rust implementation aligned

Not Proven: Implementation is correct
Not Proven: Side-channel resistance
Not Proven: Computational hardness assumptions hold forever
Not Proven: End-to-end X3DH → Double Ratchet linking

The Complete Security Picture

Result: Formal verification is ONE piece of the security puzzle. Alone, it proves protocol security. With other practices (code review, testing, monitoring), it provides comprehensive security guarantees.


Conclusion

Formal verification is essential but not sufficient.

What It Gives You:

  • Mathematical proof protocol design is secure
  • Confidence no logical flaws
  • Evidence for security audits

What It Doesn't Give You:

  • Implementation correctness (needs code review, testing)
  • Side-channel resistance (needs timing analysis, platform hardening)
  • Computational guarantees (needs crypto research monitoring)
  • End-to-end linking (needs integration testing)

Best Practice: Combine formal verification with:

  • Code review and testing
  • Timing analysis and fuzzing
  • Performance testing
  • Integration testing
  • Security monitoring
  • User education

Result: Comprehensive security from protocol design to production deployment.


Further Reading

On Formal Verification Limitations:

On Cryptographic Hardness:

On Side-Channel Resistance:

On Integration Testing:



Document Version: 1.0
Document Date: February 2026
Last Updated: February 2026
Disclaimer: This document outlines limitations as of document date. Crypto and security research is ongoing.


Remember

Formal verification proves the protocol design is secure. To ensure implementation security, you MUST also:

  1. Test your code thoroughly
  2. Review code for bugs
  3. Analyze timing side-channels
  4. Monitor cryptographic research
  5. Test performance under load
  6. Educate users on security best practices

Formal verification is a tool, not a magic wand. Use it in combination with other security practices for comprehensive security.