Test Coverage Analysis - Signal Protocol Security Testing
Overview
Comprehensive analysis of security test coverage for the Signal Protocol Rust/WASM implementation, identifying gaps and providing recommendations for production-ready security testing.
Current Security Test Coverage: 15% (18 out of ~120 required tests)
Analysis Date: February 2026 Total Tests: 120 tests across multiple modules Security-Focused Tests: ~18 tests (15%)
Executive Summary
The implementation has good functional test coverage but insufficient security-specific testing. Critical attack scenarios and edge cases are not tested.
Key Findings:
- 32 cryptographic primitive tests (good coverage)
- 28 protocol flow tests (good coverage)
- Only 18 security-specific tests (poor)
- No fuzzing tests
- No timing attack tests
- No replay attack tests
- Missing 80-100 essential security tests
Risk: Specification deviations and attack scenarios not validated by tests
Current Test Suite Overview
Test Files Analyzed
Cryptographic Primitives:
tests/crypto_test.rs- 32 tests- X25519 ECDH operations
- Ed25519 signing/verification
- AES-GCM encryption/decryption
- HKDF key derivation
Protocol Implementation:
-
tests/x3dh_test.rs- 15 tests- X3DH initiation and reception
- Key agreement flows
- Error handling
-
tests/double_ratchet_test.rs- 28 tests- DH ratchet operations
- Symmetric ratchet
- Out-of-order messages
- Skipped key management
Integration:
tests/session_test.rs- 20 tests- Full session establishment
- Message exchange
- Error scenarios
Utilities:
tests/serialization_test.rs- 18 tests- Encoding/decoding
- WASM boundary crossing
Helper Tests:
tests/keys_test.rs- 7 tests- Key generation
- Key pair creation
Total: 120 functional tests
Test Coverage by Category
Functional Testing (Current: 120 tests)
| Category | Tests | Coverage | Status |
|---|---|---|---|
| Key generation | 7 | Good | |
| ECDH operations | 12 | Good | |
| Signatures | 10 | Good | |
| Encryption/Decryption | 10 | Good | |
| X3DH protocol | 15 | Good | |
| Double Ratchet | 28 | Good | |
| Session management | 20 | Good | |
| Serialization | 18 | Good |
Security Testing (Current: ~18 tests)
| Category | Tests | Required | Gap | Status |
|---|---|---|---|---|
| Attack Scenarios | 0 | 15 | -15 | |
| Fuzzing | 0 | 10 | -10 | |
| Timing Attacks | 0 | 6 | -6 | |
| Replay Protection | 2 | 8 | -6 | |
| Message Ordering | 0 | 6 | -6 | |
| Boundary Conditions | 8 | 15 | -7 | |
| Error Path Security | 5 | 10 | -5 | |
| Negative Tests | 3 | 15 | -12 |
Total Security Test Gap: 67 missing tests
Critical Missing Test Coverage
1. Attack Scenario Testing (0/15 tests)
Required Tests:
Message Reordering Attack:
#[test]
fn test_message_reordering_attack() {
let (alice, bob) = setup_session();
let msg1 = alice.encrypt("Message 1");
let msg2 = alice.encrypt("Message 2");
let msg3 = alice.encrypt("Message 3");
// Deliver out of order
let plain2 = bob.decrypt(&msg2);
let plain1 = bob.decrypt(&msg1);
let plain3 = bob.decrypt(&msg3);
// CURRENT: All succeed (order not enforced)
// Should verify AAD prevents accepting reordered messages
// Or application tracks sequence numbers
}
X3DH Man-in-the-Middle:
#[test]
fn test_x3dh_mitm_attack() {
let alice = generate_identity_keypair();
let bob = generate_identity_keypair();
let attacker = generate_identity_keypair();
// Bob creates signed prekey
let bob_signed_prekey = generate_signed_prekey(&bob);
// Attacker substitutes their key
let malicious_prekey = attacker.public_key;
// CURRENT: Alice doesn't verify signature - accepts malicious key
let result = x3dh_initiate(
&alice,
&generate_ephemeral_keypair(),
&bob.public_key,
&malicious_prekey, // Attacker's key
None
);
// Should fail signature verification
assert!(result.is_err());
}
Replay Attack:
#[test]
fn test_replay_attack() {
let (alice, bob) = setup_session();
let encrypted = alice.encrypt("secret message");
// First decryption: should succeed
let plain1 = bob.decrypt(&encrypted).unwrap();
assert_eq!(plain1, "secret message");
// Replay attempt: should fail
let result = bob.decrypt(&encrypted);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), SignalError::ReplayDetected));
}
Epoch Rollback:
#[test]
fn test_epoch_rollback_attack() {
let (alice, bob) = setup_session();
// Advance ratchet multiple times
alice.encrypt("msg1");
alice.encrypt("msg2");
let old_state = alice.clone();
alice.encrypt("msg3");
// Try to use old state (epoch rollback)
let old_encrypted = old_state.encrypt("rollback attempt");
// Should detect old epoch and reject
let result = bob.decrypt(&old_encrypted);
assert!(result.is_err());
}
Current Coverage: 0%
2. Fuzzing Tests (0/10 tests)
Required Tests:
Fuzz X3DH Inputs:
#[test]
fn fuzz_x3dh_random_inputs() {
use quickcheck::{quickcheck, TestResult};
fn prop(
alice_priv: Vec<u8>,
bob_pub: Vec<u8>,
bob_spk: Vec<u8>
) -> TestResult {
// Should never panic, always return Result
let result = x3dh_initiate(
&alice_priv,
&generate_ephemeral_keypair(),
&bob_pub,
&bob_spk,
None
);
// Either succeeds or returns error (no panic)
TestResult::passed()
}
quickcheck(prop as fn(Vec<u8>, Vec<u8>, Vec<u8>) -> TestResult);
}
Fuzz Message Decryption:
#[test]
fn fuzz_decrypt_malformed_messages() {
use arbitrary::Arbitrary;
let (alice, bob) = setup_session();
for _ in 0..1000 {
let random_data: Vec<u8> = (0..256).map(|_| rand::random()).collect();
// Should gracefully reject, not panic
let result = bob.decrypt(&random_data);
assert!(result.is_err());
}
}
Current Coverage: 0% (no fuzzing at all)
3. Timing Attack Tests (0/6 tests)
Required Tests:
Constant-Time Key Comparison:
#[test]
fn test_constant_time_key_comparison() {
let key1 = vec![0u8; 32];
let key2_match = vec![0u8; 32];
let key2_differ_first = {
let mut k = vec![0u8; 32];
k[0] = 1;
k
};
let key2_differ_last = {
let mut k = vec![0u8; 32];
k[31] = 1;
k
};
// Measure timing for all comparisons
let start = Instant::now();
for _ in 0..100000 {
let _ = constant_time_compare(&key1, &key2_match);
}
let time_match = start.elapsed();
let start = Instant::now();
for _ in 0..100000 {
let _ = constant_time_compare(&key1, &key2_differ_first);
}
let time_differ_first = start.elapsed();
let start = Instant::now();
for _ in 0..100000 {
let _ = constant_time_compare(&key1, &key2_differ_last);
}
let time_differ_last = start.elapsed();
// Timing variance should be minimal (< 5%)
let max_time = time_match.max(time_differ_first).max(time_differ_last);
let min_time = time_match.min(time_differ_first).min(time_differ_last);
let variance = (max_time.as_nanos() - min_time.as_nanos()) as f64
/ min_time.as_nanos() as f64;
assert!(variance < 0.05, "Timing variance too high: {:.2}%", variance * 100.0);
}
Decryption Timing Analysis:
#[test]
fn test_decrypt_timing_constant() {
let (alice, bob) = setup_session();
let valid_msg = alice.encrypt("test");
let invalid_msg = vec![0u8; valid_msg.len()];
// Measure valid decryption timing
let start = Instant::now();
for _ in 0..1000 {
let _ = bob.decrypt(&valid_msg);
}
let time_valid = start.elapsed();
// Measure invalid decryption timing
let start = Instant::now();
for _ in 0..1000 {
let _ = bob.decrypt(&invalid_msg);
}
let time_invalid = start.elapsed();
// Should have similar timing (constant-time verification)
let ratio = time_valid.as_nanos() as f64 / time_invalid.as_nanos() as f64;
assert!((0.8..1.2).contains(&ratio), "Timing leak detected: ratio {:.2}", ratio);
}
Current Coverage: 0%
4. Specification Compliance Tests (0/12 tests)
Required Tests:
AAD Usage Verification:
#[test]
fn test_aad_prevents_reordering() {
let (alice, bob) = setup_session();
let msg = alice.encrypt_with_seq("test", seq_num=1);
// Modify sequence number in header (keep ciphertext)
let modified = modify_sequence_number(&msg, new_seq=99);
// Should fail: AAD binds ciphertext to header
let result = bob.decrypt(&modified);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), SignalError::AuthenticationFailed));
}
Signed Prekey Verification:
#[test]
fn test_signed_prekey_must_be_verified() {
let bob_identity = generate_identity_keypair();
let bob_signed_prekey = generate_signed_prekey(&bob_identity);
// Create invalid signature
let invalid_signature = vec![0u8; 64];
let result = x3dh_initiate(
&generate_identity_keypair(),
&generate_ephemeral_keypair(),
&bob_identity.public_key,
&bob_signed_prekey.public_key,
&invalid_signature, // Invalid signature
None
);
// MUST fail per Signal spec
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), SignalError::InvalidSignature));
}
HKDF Parameter Compliance:
#[test]
fn test_hkdf_signal_spec_compliance() {
// Test vectors from Signal specification
let ikm = hex::decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap();
let salt = hex::decode("000102030405060708090a0b0c").unwrap();
let info = b"WhisperText";
let output = hkdf_derive(&ikm, &salt, info, 32);
// Expected output from Signal test vectors
let expected = hex::decode("...").unwrap();
assert_eq!(output, expected, "HKDF output doesn't match Signal spec");
}
Current Coverage: 0%
5. Boundary Condition Tests (8/15 tests)
Existing Tests: Good coverage
- Zero-length messages
- Maximum message sizes
- Empty key packages
- Null optional parameters
Missing Tests:
Maximum Skipped Keys:
#[test]
fn test_max_skipped_keys_limit() {
let (alice, bob) = setup_session();
// Send 1001 messages (exceeds MAX_SKIP=1000)
let messages: Vec<_> = (0..1001)
.map(|i| alice.encrypt(&format!("msg{}", i)))
.collect();
// Deliver only first and last
bob.decrypt(&messages[0]).unwrap();
// Should reject or handle gracefully
let result = bob.decrypt(&messages[1000]);
assert!(result.is_err() || result.is_ok()); // Document behavior
}
Key Exhaustion:
#[test]
fn test_key_chain_exhaustion() {
let (alice, _bob) = setup_session();
// Send maximum number of messages in single chain
for i in 0..u32::MAX {
let result = alice.encrypt(&format!("msg{}", i));
if result.is_err() {
// Should handle overflow gracefully
assert!(i > 1_000_000); // Should support many messages
break;
}
}
}
Current Coverage: 53% (8/15 tests)
6. Error Path Security Tests (5/10 tests)
Existing Tests:
- Invalid key lengths
- Malformed messages
- Decryption failures
- Serialization errors
- WASM boundary errors
Missing Tests:
No Secrets in Error Messages:
#[test]
fn test_no_secrets_in_errors() {
let (alice, bob) = setup_session();
let encrypted = alice.encrypt("secret data");
// Corrupt ciphertext
let mut corrupted = encrypted.clone();
corrupted[10] ^= 0xFF;
let result = bob.decrypt(&corrupted);
let error_msg = format!("{:?}", result.unwrap_err());
// Error message should NOT contain:
assert!(!error_msg.contains("secret"));
assert!(!error_msg.to_lowercase().contains("key"));
assert!(!error_msg.contains("0x")); // No hex dumps
}
Error Timing Consistency:
#[test]
fn test_error_paths_constant_time() {
let (alice, bob) = setup_session();
let valid = alice.encrypt("test");
let invalid_tag = {
let mut m = valid.clone();
m[m.len() - 1] ^= 1; // Corrupt auth tag
m
};
let invalid_length = vec![0u8; 10];
// All error paths should have similar timing
let timings = vec![
time_operation(|| bob.decrypt(&invalid_tag)),
time_operation(|| bob.decrypt(&invalid_length)),
time_operation(|| bob.decrypt(&vec![0u8; valid.len()])),
];
let max_variance = calculate_variance(&timings);
assert!(max_variance < 0.1, "Error path timing leak");
}
Current Coverage: 50% (5/10 tests)
Test Quality Assessment
Existing Test Analysis
Example: Good Test
#[test]
fn test_double_ratchet_forward_secrecy() {
let (alice, bob) = setup_session();
let msg1 = alice.encrypt("message 1");
bob.decrypt(&msg1).unwrap();
// Advance ratchet
alice.encrypt("message 2");
// Old message should not decrypt with new state
let result = bob.decrypt(&msg1);
assert!(result.is_err()); // Tests security property
}
Assessment:
- Tests security property (forward secrecy)
- Clear intent
- Verifies correct rejection
Example: Weak Test
#[test]
fn test_x3dh_initiate() {
let result = x3dh_initiate(/*params*/);
assert!(result.is_ok()); // Only tests success path
}
Assessment:
- Doesn't test security properties
- Missing negative tests
- Doesn't verify signed prekey handling
- No attack scenario testing
Recommended Security Test Suite
Phase 1: Critical Security Tests (30 tests, 1-2 weeks)
Group 1: Attack Scenarios (15 tests)
describe("Signal Protocol Attack Scenarios", || {
test("Message reordering attack");
test("X3DH MITM with invalid signature");
test("Replay attack within window");
test("Replay attack outside window");
test("Epoch rollback attempt");
test("Session confusion attack");
test("Prekey exhaustion attack");
test("Denial of service via skipped keys");
test("Message deletion attack");
test("Out-of-order boundary attack");
test("Cross-session message injection");
test("Identity key confusion");
test("Concurrent session establishment");
test("Ratchet state corruption");
test("Key compromise recovery");
});
Group 2: Specification Compliance (15 tests)
describe("Signal Specification Compliance", || {
test("AAD includes message metadata");
test("Signed prekey signature verified");
test("HKDF parameters match spec");
test("X3DH DH ordering correct");
test("Double Ratchet KDF usage correct");
test("Message format per spec");
test("Prekey bundle format correct");
test("Session establishment per spec");
test("Key rotation timing correct");
test("Error codes match spec");
test("Nonce generation correct");
test("Chain key derivation correct");
test("Message key derivation correct");
test("Root key update correct");
test("Interoperability with libsignal");
});
Phase 2: Fuzzing & Property Tests (20 tests, 1-2 weeks)
Group 3: Fuzz Testing (10 tests)
describe("Fuzzing Tests", || {
test("Fuzz X3DH inputs (random keys)");
test("Fuzz message decryption (random data)");
test("Fuzz prekey bundle parsing");
test("Fuzz session state deserialiation");
test("Fuzz WASM boundary (invalid JSON)");
test("Fuzz key generation (edge cases)");
test("Fuzz signature verification");
test("Fuzz ECDH operations");
test("Fuzz HKDF inputs");
test("Fuzz AES-GCM encryption");
});
Group 4: Property-Based Tests (10 tests)
describe("Property-Based Tests", || {
test("Encryption/decryption roundtrip");
test("Key derivation deterministic");
test("Signature verify iff valid");
test("ECDH commutativity");
test("Ratchet state consistency");
test("Message ordering invariants");
test("Key chain monotonicity");
test("Session state serialization");
test("Error conditions transient");
test("Memory bounds respected");
});