// Security coverage for relay TLS/mTLS fail-closed policy. // // Scope: exercise local TLS policy helpers and preserve installer/runtime // markers that make production relay access client-authenticated. // Targets: `server/src/security.rs`, `client/src/relay_transport.rs`, and // install-time PKI handling. // Why: HID, clipboard, mic, and camera RPCs are too powerful to rely on // application-level good behavior; production trust must be enforced at the // transport boundary. use lesavka_client::relay_transport::{endpoint, enforce_transport_policy}; use lesavka_server::security::server_tls_config; use serial_test::serial; use temp_env::with_vars; use tempfile::tempdir; const SERVER_INSTALL: &str = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/scripts/install/server.sh" )); const CLIENT_INSTALL: &str = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/scripts/install/client.sh" )); const SERVER_SECURITY: &str = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/server/src/security.rs" )); const CLIENT_TRANSPORT: &str = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/client/src/relay_transport.rs" )); const SERVER_ENTRYPOINT: &str = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/server/src/main/entrypoint.rs" )); #[test] #[serial] fn production_tls_requires_server_identity_files() { with_vars( [ ("LESAVKA_REQUIRE_TLS", Some("1")), ("LESAVKA_TLS_CERT", Some("/tmp/lesavka-missing-server.crt")), ("LESAVKA_TLS_KEY", Some("/tmp/lesavka-missing-server.key")), ("LESAVKA_TLS_CLIENT_CA", Some("/tmp/lesavka-missing-ca.crt")), ], || { let err = server_tls_config().expect_err("required TLS must fail closed"); assert!(err.to_string().contains("LESAVKA_REQUIRE_TLS=1")); }, ); } #[test] #[serial] fn server_tls_config_accepts_identity_and_keeps_client_ca_binding_explicit() { let dir = tempdir().expect("tls dir"); let cert = dir.path().join("server.crt"); let key = dir.path().join("server.key"); let ca = dir.path().join("ca.crt"); std::fs::write(&cert, b"not a real cert").expect("write cert"); std::fs::write(&key, b"not a real key").expect("write key"); std::fs::write(&ca, b"not a real ca").expect("write ca"); with_vars( [ ("LESAVKA_REQUIRE_TLS", Some("1")), ("LESAVKA_TLS_CERT", Some(cert.to_string_lossy().as_ref())), ("LESAVKA_TLS_KEY", Some(key.to_string_lossy().as_ref())), ("LESAVKA_TLS_CLIENT_CA", Some(ca.to_string_lossy().as_ref())), ("LESAVKA_TLS_CLIENT_AUTH_OPTIONAL", Some("0")), ], || { assert!( server_tls_config() .expect("identity and CA should build TLS config") .is_some() ); }, ); assert!(SERVER_SECURITY.contains("client_ca_root")); assert!(SERVER_SECURITY.contains("LESAVKA_TLS_CLIENT_AUTH_OPTIONAL")); } #[test] #[serial] fn client_https_endpoint_fails_fast_without_enrollment() { let dir = tempdir().expect("empty home"); with_vars( [ ("HOME", Some(dir.path().to_string_lossy().as_ref())), ("LESAVKA_TLS_CA", None::<&str>), ("LESAVKA_TLS_CLIENT_CERT", None::<&str>), ("LESAVKA_TLS_CLIENT_KEY", None::<&str>), ("LESAVKA_ALLOW_INSECURE", None::<&str>), ], || { let err = endpoint("https://38.28.125.112:50051") .expect_err("missing client cert must fail before connecting"); let rendered = format!("{err:#}"); assert!(rendered.contains("TLS enrollment is missing")); assert!(rendered.contains("client.key")); }, ); } #[test] #[serial] fn client_https_endpoint_rejects_malformed_enrollment_material() { let dir = tempdir().expect("pki home"); let pki = dir.path().join(".config/lesavka/pki"); std::fs::create_dir_all(&pki).expect("create pki"); std::fs::write(pki.join("ca.crt"), b"wrong ca").expect("write ca"); std::fs::write(pki.join("client.crt"), b"malformed cert").expect("write cert"); std::fs::write(pki.join("client.key"), b"malformed key").expect("write key"); with_vars( [ ("HOME", Some(dir.path().to_string_lossy().as_ref())), ("LESAVKA_TLS_CA", None::<&str>), ("LESAVKA_TLS_CLIENT_CERT", None::<&str>), ("LESAVKA_TLS_CLIENT_KEY", None::<&str>), ], || { let err = endpoint("https://lesavka-server:50051") .expect_err("malformed cert/key material must fail"); assert!(format!("{err:#}").contains("configuring relay TLS")); }, ); } #[test] #[serial] fn public_plaintext_transport_requires_deliberate_lab_override() { with_vars([("LESAVKA_ALLOW_INSECURE", None::<&str>)], || { let err = enforce_transport_policy("http://38.28.125.112:50051") .expect_err("public plaintext relay should be rejected"); assert!( err.to_string() .contains("refusing insecure relay transport") ); }); with_vars([("LESAVKA_ALLOW_INSECURE", Some("1"))], || { enforce_transport_policy("http://38.28.125.112:50051") .expect("explicit lab override should allow plaintext"); }); } #[test] fn install_and_runtime_paths_keep_mtls_client_identity_first_class() { for marker in [ "LESAVKA_REQUIRE_TLS:-1", "LESAVKA_TLS_CLIENT_CA", "openssl genrsa -out \"$LESAVKA_TLS_DIR/client.key\"", "lesavka-client-pki.tar.gz", "chmod 0600 \"$LESAVKA_TLS_DIR/\"*.key", ] { assert!( SERVER_INSTALL.contains(marker), "server installer should preserve mTLS marker {marker}" ); } for marker in [ "LESAVKA_CLIENT_PKI_BUNDLE", "ca.crt", "client.crt", "client.key", "install -m 0600", ] { assert!( CLIENT_INSTALL.contains(marker), "client installer should preserve enrollment marker {marker}" ); } assert!(SERVER_ENTRYPOINT.contains("security::server_tls_config()?")); assert!(SERVER_ENTRYPOINT.contains(".tls_config(tls)?")); assert!(CLIENT_TRANSPORT.contains("tls.identity(Identity::from_pem(cert, key))")); assert!(CLIENT_TRANSPORT.contains("tls.ca_certificate(Certificate::from_pem(ca))")); }