qssh 0.0.2-alpha

Experimental quantum-safe SSH using post-quantum crypto. Research project - NOT for production. See LIMITATIONS.md
Documentation
//! Full integration tests for QSSH client-server functionality

use qssh::{
    QsshClient, QsshServer, QsshConfig, QsshServerConfig,
    crypto::{PqKeyExchange, PqAlgorithm},
};
use tokio::time::{sleep, Duration, timeout};
use tempfile::tempdir;
use std::process::Command;
use std::path::PathBuf;
use tokio::fs;

#[tokio::test]
async fn test_full_client_server_connection() {
    // Setup test directory
    let dir = tempdir().unwrap();
    let test_path = dir.path().to_path_buf();
    
    // Generate host key
    let host_key_path = test_path.join("host_key");
    let host_key = PqKeyExchange::new().unwrap();
    let host_private = host_key.secret_bytes();
    let host_public = host_key.public_bytes();
    
    // Save host key in PEM format
    let host_pem = format!(
        "-----BEGIN QSSH PRIVATE KEY-----\n\
         Algorithm: Falcon512\n\
         {}\n\
         -----END QSSH PRIVATE KEY-----\n",
        base64::engine::general_purpose::STANDARD.encode(&host_private)
    );
    fs::write(&host_key_path, host_pem).await.unwrap();
    
    // Generate client identity key
    let client_key_path = test_path.join("id_qssh");
    let client_key = PqKeyExchange::new().unwrap();
    let client_private = client_key.secret_bytes();
    let client_public = client_key.public_bytes();
    
    // Save client key in PEM format
    let client_pem = format!(
        "-----BEGIN QSSH PRIVATE KEY-----\n\
         Algorithm: Falcon512\n\
         {}\n\
         -----END QSSH PRIVATE KEY-----\n",
        base64::engine::general_purpose::STANDARD.encode(&client_private)
    );
    fs::write(&client_key_path, client_pem).await.unwrap();
    
    // Save client public key
    let client_pubkey_path = test_path.join("id_qssh.pub");
    let client_pubkey_line = format!(
        "qssh-falcon512 {} test@localhost\n",
        base64::engine::general_purpose::STANDARD.encode(&client_public)
    );
    fs::write(&client_pubkey_path, client_pubkey_line).await.unwrap();
    
    // Create authorized_keys
    let auth_dir = test_path.join("testuser").join(".ssh");
    fs::create_dir_all(&auth_dir).await.unwrap();
    let auth_keys_path = auth_dir.join("authorized_keys");
    fs::write(&auth_keys_path, &client_pubkey_line).await.unwrap();
    
    // Start server
    let server_config = QsshServerConfig::new("127.0.0.1:0").unwrap();
    let server = QsshServer::new(server_config);
    
    // Set environment for auth path
    std::env::set_var("QSSH_AUTH_PATH", test_path.to_str().unwrap());
    
    let server_handle = tokio::spawn(async move {
        let _ = timeout(Duration::from_secs(5), server.start()).await;
    });
    
    // Give server time to start
    sleep(Duration::from_millis(500)).await;
    
    // Create client config
    let config = QsshConfig {
        server: "127.0.0.1:22222".to_string(), // Will need to get actual port
        username: "testuser".to_string(),
        port_forwards: vec![],
        use_qkd: false,
        pq_algorithm: PqAlgorithm::Falcon512,
        key_rotation_interval: 3600,
    };
    
    // Connect client
    std::env::set_var("HOME", test_path.to_str().unwrap());
    let mut client = QsshClient::new(config);
    
    // Test connection with timeout
    let connect_result = timeout(Duration::from_secs(3), client.connect()).await;
    
    // Clean up
    server_handle.abort();
    
    // For now, we just verify no panic occurred
    // In a real test, we'd verify successful connection
    assert!(connect_result.is_ok() || connect_result.is_err());
}

#[tokio::test]
async fn test_authentication_flow() {
    // Setup test environment
    let dir = tempdir().unwrap();
    let test_path = dir.path().to_path_buf();
    
    // Generate two key pairs - one authorized, one not
    let authorized_key = PqKeyExchange::new().unwrap();
    let unauthorized_key = PqKeyExchange::new().unwrap();
    
    // Setup authorized_keys with only the first key
    let auth_dir = test_path.join("user1").join(".ssh");
    fs::create_dir_all(&auth_dir).await.unwrap();
    
    let authorized_pubkey = format!(
        "qssh-falcon512 {} user1@test\n",
        base64::engine::general_purpose::STANDARD.encode(&authorized_key.public_bytes())
    );
    
    let auth_keys_path = auth_dir.join("authorized_keys");
    fs::write(&auth_keys_path, authorized_pubkey).await.unwrap();
    
    // Save both private keys
    let auth_key_path = test_path.join("authorized_key");
    let auth_pem = format!(
        "-----BEGIN QSSH PRIVATE KEY-----\n\
         Algorithm: Falcon512\n\
         {}\n\
         -----END QSSH PRIVATE KEY-----\n",
        base64::engine::general_purpose::STANDARD.encode(&authorized_key.secret_bytes())
    );
    fs::write(&auth_key_path, auth_pem).await.unwrap();
    
    let unauth_key_path = test_path.join("unauthorized_key");
    let unauth_pem = format!(
        "-----BEGIN QSSH PRIVATE KEY-----\n\
         Algorithm: Falcon512\n\
         {}\n\
         -----END QSSH PRIVATE KEY-----\n",
        base64::engine::general_purpose::STANDARD.encode(&unauthorized_key.secret_bytes())
    );
    fs::write(&unauth_key_path, unauth_pem).await.unwrap();
    
    // Test that authorized key works
    std::env::set_var("QSSH_AUTH_PATH", test_path.to_str().unwrap());
    
    // In a real test, we'd start a server and try both keys
    // For now, we verify the setup is correct
    assert!(auth_key_path.exists());
    assert!(unauth_key_path.exists());
    assert!(auth_keys_path.exists());
}

#[tokio::test]
async fn test_command_execution() {
    // This test would verify that commands can be executed remotely
    // For now, we test the command parsing
    
    let test_commands = vec![
        ("echo 'hello'", true),
        ("ls -la", true),
        ("whoami", true),
        ("cat /etc/passwd", true),
        ("", false), // Empty command should fail
    ];
    
    for (cmd, should_succeed) in test_commands {
        if should_succeed {
            assert!(!cmd.is_empty());
        } else {
            assert!(cmd.is_empty());
        }
    }
}

#[tokio::test]
async fn test_key_rotation() {
    // Test that keys can be rotated during a session
    let kex1 = PqKeyExchange::new().unwrap();
    let kex2 = PqKeyExchange::new().unwrap();
    
    // Simulate key rotation
    let old_public = kex1.public_bytes();
    let new_public = kex2.public_bytes();
    
    assert_ne!(old_public, new_public);
    assert_eq!(old_public.len(), 897); // Falcon-512 public key size
    assert_eq!(new_public.len(), 897);
}

#[tokio::test]
async fn test_concurrent_connections() {
    // Test that server can handle multiple concurrent connections
    let num_clients = 5;
    let mut handles = vec![];
    
    for i in 0..num_clients {
        let handle = tokio::spawn(async move {
            // Simulate client connection
            sleep(Duration::from_millis(i * 100)).await;
            format!("Client {} connected", i)
        });
        handles.push(handle);
    }
    
    // Wait for all clients
    let results: Vec<_> = futures::future::join_all(handles).await;
    
    assert_eq!(results.len(), num_clients);
    for result in results {
        assert!(result.is_ok());
    }
}

#[tokio::test]
async fn test_graceful_disconnection() {
    // Test that disconnection is handled gracefully
    use qssh::transport::{Message, DisconnectMessage};
    use qssh::transport::protocol::disconnect_reasons;
    
    let disconnect_msg = DisconnectMessage {
        reason_code: disconnect_reasons::BY_APPLICATION,
        description: "Test disconnection".to_string(),
    };
    
    let msg = Message::Disconnect(disconnect_msg.clone());
    
    // Serialize and deserialize to verify
    let serialized = bincode::serialize(&msg).unwrap();
    let deserialized: Message = bincode::deserialize(&serialized).unwrap();
    
    if let Message::Disconnect(d) = deserialized {
        assert_eq!(d.reason_code, disconnect_reasons::BY_APPLICATION);
        assert_eq!(d.description, "Test disconnection");
    } else {
        panic!("Wrong message type");
    }
}

#[tokio::test]
async fn test_port_forwarding_config() {
    use qssh::PortForward;
    
    let forward = PortForward {
        local_port: 8080,
        remote_host: "localhost".to_string(),
        remote_port: 80,
    };
    
    assert_eq!(forward.local_port, 8080);
    assert_eq!(forward.remote_host, "localhost");
    assert_eq!(forward.remote_port, 80);
    
    // Test parsing port forward specification
    let spec = "8080:localhost:80";
    let parts: Vec<&str> = spec.split(':').collect();
    assert_eq!(parts.len(), 3);
    
    let parsed = PortForward {
        local_port: parts[0].parse().unwrap(),
        remote_host: parts[1].to_string(),
        remote_port: parts[2].parse().unwrap(),
    };
    
    assert_eq!(parsed.local_port, forward.local_port);
    assert_eq!(parsed.remote_host, forward.remote_host);
    assert_eq!(parsed.remote_port, forward.remote_port);
}

#[tokio::test] 
async fn test_invalid_protocol_version() {
    use qssh::transport::{ClientHelloMessage, Message};
    
    let invalid_hello = ClientHelloMessage {
        version: 999, // Invalid version
        random: vec![0; 32],
        kex_algorithms: vec![PqAlgorithm::Falcon512],
        sig_algorithms: vec![PqAlgorithm::SphincsPlus],
        ciphers: vec!["aes256-gcm".to_string()],
        qkd_capable: false,
        extensions: vec![],
    };
    
    // Server should reject this
    assert_ne!(invalid_hello.version, 1); // PROTOCOL_VERSION is 1
}

#[tokio::test]
async fn test_message_size_limits() {
    // Test that oversized messages are handled properly
    let huge_data = vec![0u8; 10_000_000]; // 10MB
    
    // This should be rejected by the transport layer
    assert!(huge_data.len() > 1_000_000); // Typical max message size
}

#[test]
fn test_algorithm_negotiation() {
    // Test that client and server can negotiate algorithms
    let client_algos = vec![
        PqAlgorithm::Falcon512,
        PqAlgorithm::Kyber512,
    ];
    
    let server_algos = vec![
        PqAlgorithm::SphincsPlus,
        PqAlgorithm::Falcon512,
    ];
    
    // Find common algorithm
    let mut common = None;
    for client_algo in &client_algos {
        if server_algos.contains(client_algo) {
            common = Some(client_algo.clone());
            break;
        }
    }
    
    assert_eq!(common, Some(PqAlgorithm::Falcon512));
}