qssh 0.4.3

Post-quantum secure shell with NIST PQC algorithms (Falcon, SPHINCS+, ML-KEM), configurable security tiers, and quantum-resistant protocol design
Documentation
// QSSH Security Test Suite
// Comprehensive security testing including fuzzing, penetration, and attack simulation

use std::process::Command;
use std::time::{Duration, Instant};
use std::net::TcpStream;
use std::io::{Write, Read};
use rand::{Rng, thread_rng};

#[cfg(test)]
mod security_tests {
    use super::*;

    // ============ FUZZING TESTS ============

    /// Fuzz test the SSH banner exchange
    #[test]
    fn fuzz_banner_exchange() {
        println!("🔨 Fuzzing SSH banner exchange...");

        let long_banner = b"SSH-2.0-".repeat(1000);
        let binary_data = [0xFF; 1024];
        let fuzz_banners: Vec<&[u8]> = vec![
            b"SSH-2.0-QSSH_1.0\r\n",  // Valid
            long_banner.as_slice(),  // Long banner
            b"SSH-1.0-OLD\r\n",  // Old version
            b"NOT_SSH\r\n",  // Invalid protocol
            b"SSH-2.0-\x00\x01\x02\x03\r\n",  // Binary in banner
            b"SSH-2.0-\xF0\x9F\x98\x80Unicode\xF0\x9F\x98\x80\r\n",  // Unicode (emoji as UTF-8 bytes)
            b"",  // Empty
            &binary_data,  // Pure binary
            b"SSH-2.0-\nNewline\nInjection\r\n",  // Newline injection
        ];

        for (i, banner) in fuzz_banners.iter().enumerate() {
            match TcpStream::connect("127.0.0.1:22222") {
                Ok(mut stream) => {
                    let _ = stream.write_all(banner);
                    let mut response = vec![0; 256];
                    let _ = stream.read(&mut response);
                    println!("  Test {}: Sent {} bytes, got response", i, banner.len());
                }
                Err(_) => println!("  Test {}: Connection refused (good!)", i),
            }
        }
    }

    /// Fuzz test key exchange packets
    #[test]
    fn fuzz_key_exchange() {
        println!("🔨 Fuzzing key exchange packets...");

        let mut rng = thread_rng();

        for size in [0, 1, 100, 1000, 65535, 1_000_000] {
            let mut packet = vec![0u8; size];
            rng.fill(&mut packet[..]);

            match TcpStream::connect("127.0.0.1:22222") {
                Ok(mut stream) => {
                    // Send valid banner first
                    let _ = stream.write_all(b"SSH-2.0-FuzzClient\r\n");
                    std::thread::sleep(Duration::from_millis(100));

                    // Send fuzzed KEX packet
                    let _ = stream.write_all(&packet);

                    // Check if server is still alive
                    let mut response = vec![0; 256];
                    match stream.read(&mut response) {
                        Ok(n) => println!("  Size {}: Server responded with {} bytes", size, n),
                        Err(e) => println!("  Size {}: Server closed connection: {}", size, e),
                    }
                }
                Err(_) => println!("  Size {}: Connection refused", size),
            }
        }
    }

    // ============ PENETRATION TESTS ============

    /// Test authentication bypass attempts
    #[test]
    fn pentest_auth_bypass() {
        println!("🔓 Testing authentication bypass attempts...");

        let bypass_attempts = vec![
            ("", ""),  // Empty credentials
            ("root", ""),  // Empty password
            ("admin", "admin"),  // Default creds
            ("qssh", "qssh"),  // Product name
            ("' OR '1'='1", "password"),  // SQL injection
            ("admin\x00extra", "password"),  // Null byte injection
            ("\r\nUSER admin", "password"),  // Command injection
            ("../../etc/passwd", "password"),  // Path traversal
        ];

        for (user, pass) in bypass_attempts {
            let output = Command::new("timeout")
                .arg("2")
                .arg("./target/debug/qssh")
                .arg("-o")
                .arg("StrictHostKeyChecking=no")
                .arg(format!("{}@127.0.0.1:22222", user))
                .env("QSSH_PASSWORD", pass)
                .output();

            match output {
                Ok(out) => {
                    if out.status.success() {
                        panic!("❌ AUTH BYPASS: user='{}' pass='{}' succeeded!", user, pass);
                    } else {
                        println!("  ✓ Rejected: user='{}' pass='{}'", user, pass);
                    }
                }
                Err(_) => println!("  ✓ Timeout/error for user='{}'", user),
            }
        }
    }

    /// Test quantum algorithm downgrade attacks
    #[test]
    fn pentest_quantum_downgrade() {
        println!("🔐 Testing quantum algorithm downgrade attacks...");

        // Try to force weak/classic algorithms
        let downgrade_attempts = vec![
            "--pq-algo=none",
            "--pq-algo=rsa",
            "--pq-algo=classical",
            "--pq-algo=weak_falcon",
            "--force-classic-crypto",
        ];

        for attempt in downgrade_attempts {
            let output = Command::new("./target/debug/qssh")
                .arg(attempt)
                .arg("test@127.0.0.1:22222")
                .arg("-vv")
                .output();

            if let Ok(out) = output {
                let stdout = String::from_utf8_lossy(&out.stdout);
                let stderr = String::from_utf8_lossy(&out.stderr);

                if stdout.contains("classical") || stderr.contains("RSA") {
                    panic!("❌ QUANTUM DOWNGRADE: {} allowed classical crypto!", attempt);
                } else {
                    println!("  ✓ Rejected downgrade: {}", attempt);
                }
            }
        }
    }

    // ============ TIMING ATTACKS ============

    /// Test timing attacks on authentication
    #[test]
    fn timing_attack_auth() {
        println!("⏱️  Testing timing attacks on authentication...");

        let test_users = vec![
            "root",      // Likely exists
            "admin",     // Might exist
            "qsshtest",  // Unlikely
            "zzz999xxx", // Very unlikely
        ];

        let mut timings = Vec::new();

        for user in &test_users {
            let mut user_times = Vec::new();

            // Multiple attempts for statistical significance
            for _ in 0..5 {
                let start = Instant::now();

                let _ = Command::new("timeout")
                    .arg("1")
                    .arg("./target/debug/qssh")
                    .arg(format!("{}@127.0.0.1:22222", user))
                    .env("QSSH_PASSWORD", "wrongpass")
                    .output();

                let elapsed = start.elapsed();
                user_times.push(elapsed.as_millis());
            }

            let avg_time = user_times.iter().sum::<u128>() / user_times.len() as u128;
            timings.push((user.to_string(), avg_time));
            println!("  User '{}': avg {}ms", user, avg_time);
        }

        // Check if timing differences reveal user existence
        let max_diff = timings.iter().map(|(_, t)| t).max().unwrap()
                     - timings.iter().map(|(_, t)| t).min().unwrap();

        if max_diff > 50 {  // 50ms difference threshold
            println!("  ⚠️  WARNING: Timing differences >50ms detected!");
            println!("  Could reveal user enumeration!");
        } else {
            println!("  ✓ Timing differences negligible (<50ms)");
        }
    }

    // ============ RESOURCE EXHAUSTION ============

    /// Test connection flooding
    #[test]
    fn dos_connection_flood() {
        println!("🌊 Testing connection flooding resistance...");

        let mut connections = Vec::new();
        let start = Instant::now();

        // Try to open many connections
        for i in 0..1000 {
            match TcpStream::connect("127.0.0.1:22222") {
                Ok(stream) => {
                    connections.push(stream);
                    if i % 100 == 0 {
                        println!("  Opened {} connections...", i);
                    }
                }
                Err(e) => {
                    println!("  Connection {} rejected: {} (good!)", i, e);
                    break;
                }
            }

            if start.elapsed() > Duration::from_secs(5) {
                println!("  Timeout after {} connections", i);
                break;
            }
        }

        println!("  Max connections achieved: {}", connections.len());

        if connections.len() > 500 {
            println!("  ⚠️  WARNING: Server allows >500 concurrent connections!");
        } else {
            println!("  ✓ Connection limits appear reasonable");
        }
    }

    /// Test memory exhaustion via large packets
    #[test]
    fn dos_memory_exhaustion() {
        println!("💾 Testing memory exhaustion attacks...");

        let sizes = vec![
            1024 * 1024,      // 1MB
            10 * 1024 * 1024, // 10MB
            100 * 1024 * 1024, // 100MB
        ];

        for size in sizes {
            match TcpStream::connect("127.0.0.1:22222") {
                Ok(mut stream) => {
                    // Send valid banner
                    let _ = stream.write_all(b"SSH-2.0-MemTest\r\n");

                    // Try to send huge packet
                    let big_data = vec![0x41u8; size];
                    match stream.write_all(&big_data) {
                        Ok(_) => println!("  Sent {}MB packet", size / 1024 / 1024),
                        Err(e) => println!("  Failed to send {}MB: {}", size / 1024 / 1024, e),
                    }

                    // Check if server still responsive
                    std::thread::sleep(Duration::from_millis(500));
                    match TcpStream::connect("127.0.0.1:22222") {
                        Ok(_) => println!("  ✓ Server still responsive after {}MB", size / 1024 / 1024),
                        Err(_) => println!("  ❌ Server unresponsive after {}MB!", size / 1024 / 1024),
                    }
                }
                Err(_) => println!("  Connection refused"),
            }
        }
    }

    // ============ QUANTUM-SPECIFIC ATTACKS ============

    /// Test quantum key reuse detection
    #[test]
    fn quantum_key_reuse() {
        println!("🔑 Testing quantum key reuse detection...");

        // This would require intercepting QKD keys
        // For now, test if same session keys are ever reused

        let mut session_keys = Vec::new();

        for i in 0..5 {
            let output = Command::new("./target/debug/qssh")
                .arg("-vvv")  // Verbose to see key exchange
                .arg("test@127.0.0.1:22222")
                .arg("-o")
                .arg("BatchMode=yes")
                .output();

            if let Ok(out) = output {
                let stderr = String::from_utf8_lossy(&out.stderr);

                // Look for session key in debug output
                if stderr.contains("session_key") || stderr.contains("shared_secret") {
                    // Extract key (simplified - would need proper parsing)
                    let key_hash = format!("{:x}", i); // Placeholder

                    if session_keys.contains(&key_hash) {
                        panic!("❌ QUANTUM KEY REUSE: Same session key used twice!");
                    }
                    session_keys.push(key_hash);
                }
            }
        }

        println!("  ✓ No key reuse detected in {} connections", session_keys.len());
    }

    /// Test STARK proof validation bypasses
    #[test]
    fn quantum_stark_bypass() {
        println!("🎯 Testing STARK proof validation...");

        // Try to send invalid STARK proofs
        match TcpStream::connect("127.0.0.1:22222") {
            Ok(mut stream) => {
                // Send banner
                let _ = stream.write_all(b"SSH-2.0-STARKTest\r\n");

                // Send packet claiming to have STARK proof but with invalid data
                let fake_proof = b"STARK_PROOF:INVALID_DATA_HERE";
                let _ = stream.write_all(fake_proof);

                let mut response = vec![0; 256];
                match stream.read(&mut response) {
                    Ok(n) => {
                        let resp_str = String::from_utf8_lossy(&response[..n]);
                        if resp_str.contains("accepted") {
                            panic!("❌ STARK BYPASS: Invalid proof accepted!");
                        } else {
                            println!("  ✓ Invalid STARK proof rejected");
                        }
                    }
                    Err(_) => println!("  ✓ Connection closed on invalid proof"),
                }
            }
            Err(_) => println!("  Connection failed"),
        }
    }
}