185 lines
6.4 KiB
Rust
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))"));
|
|
}
|