lesavka/tests/security/server/tls/server_tls_security_contract.rs

185 lines
6.4 KiB
Rust

// 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))"));
}