use oa_gateway_adapter::tls::{client_tls, server_tls, ClientTls, ServerTls};
use crate::addr::host_part;
use crate::config::Config;
#[derive(Debug)]
pub(crate) struct HostTls {
pub(crate) owp: Option<ServerTls>,
pub(crate) stomp: Option<ClientTls>,
}
pub(crate) fn load(config: &Config) -> Result<HostTls, String> {
let owp = if config.owp.enabled {
let cert = non_empty_path(&config.owp.tls_cert);
let key = non_empty_path(&config.owp.tls_key);
let client_ca = non_empty_path(&config.owp.tls_client_ca);
server_tls("owp.tls", cert, key, client_ca)?
} else {
None
};
let stomp = if config.stomp.enabled {
if config.stomp.tls {
let ca = non_empty_path(&config.stomp.tls_ca);
let name = if config.stomp.tls_server_name.is_empty() {
host_part(&config.stomp.broker)
} else {
&config.stomp.tls_server_name
};
let client_cert = non_empty_path(&config.stomp.tls_client_cert);
let client_key = non_empty_path(&config.stomp.tls_client_key);
Some(client_tls("stomp.tls", ca, name, client_cert, client_key)?)
} else if !config.stomp.tls_client_cert.is_empty() {
return Err(
"stomp.tls_client_cert is set but stomp.tls is not. A client certificate \
needs stomp.tls = true to matter."
.into(),
);
} else if !config.stomp.tls_client_key.is_empty() {
return Err(
"stomp.tls_client_key is set but stomp.tls is not. A client certificate \
needs stomp.tls = true to matter."
.into(),
);
} else {
None
}
} else {
None
};
Ok(HostTls { owp, stomp })
}
fn non_empty_path(value: &str) -> Option<&std::path::Path> {
if value.is_empty() {
None
} else {
Some(std::path::Path::new(value))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_tls_configured_leaves_owp_plaintext() {
let config: Config = toml::from_str("[owp]\nenabled = true\n").unwrap();
assert!(load(&config).unwrap().owp.is_none());
}
#[test]
fn a_disabled_owp_adapter_is_not_checked_for_tls() {
let config: Config =
toml::from_str("[owp]\nenabled = false\ntls_cert = \"definitely/not/here.pem\"\n")
.unwrap();
assert!(load(&config).unwrap().owp.is_none());
}
#[test]
fn a_cert_without_a_key_is_refused_at_startup() {
let config: Config =
toml::from_str("[owp]\nenabled = true\ntls_cert = \"definitely/not/here.pem\"\n")
.unwrap();
let err = load(&config).unwrap_err();
assert!(err.contains("owp.tls_key"), "{err}");
}
#[test]
fn an_unreadable_cert_path_names_the_file() {
let config: Config = toml::from_str(
"[owp]\nenabled = true\ntls_cert = \"definitely/not/here.pem\"\ntls_key = \"definitely/not/here-key.pem\"\n",
)
.unwrap();
let err = load(&config).unwrap_err();
assert!(err.contains("definitely/not/here.pem"), "{err}");
}
#[test]
fn a_matching_cert_and_key_are_loaded() {
let rcgen::CertifiedKey { cert, key_pair } =
rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
let dir = std::env::temp_dir().join("oa-gateway-tls-test");
std::fs::create_dir_all(&dir).unwrap();
let cert_path = dir.join("cert.pem");
let key_path = dir.join("key.pem");
std::fs::write(&cert_path, cert.pem()).unwrap();
std::fs::write(&key_path, key_pair.serialize_pem()).unwrap();
let config: Config = toml::from_str(&format!(
"[owp]\nenabled = true\ntls_cert = {:?}\ntls_key = {:?}\n",
cert_path.display().to_string(),
key_path.display().to_string(),
))
.unwrap();
assert!(load(&config).unwrap().owp.is_some());
std::fs::remove_file(&cert_path).ok();
std::fs::remove_file(&key_path).ok();
}
#[test]
fn a_client_ca_without_a_cert_or_key_is_refused_at_startup() {
let config: Config =
toml::from_str("[owp]\nenabled = true\ntls_client_ca = \"definitely/not/here.pem\"\n")
.unwrap();
let err = load(&config).unwrap_err();
assert!(err.contains("owp.tls_client_ca"), "{err}");
}
#[test]
fn a_cert_key_and_client_ca_load_together() {
let rcgen::CertifiedKey { cert, key_pair } =
rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
let dir = std::env::temp_dir().join("oa-gateway-mtls-test");
std::fs::create_dir_all(&dir).unwrap();
let cert_path = dir.join("cert.pem");
let key_path = dir.join("key.pem");
let client_ca_path = dir.join("client-ca.pem");
std::fs::write(&cert_path, cert.pem()).unwrap();
std::fs::write(&key_path, key_pair.serialize_pem()).unwrap();
std::fs::write(&client_ca_path, cert.pem()).unwrap();
let config: Config = toml::from_str(&format!(
"[owp]\nenabled = true\ntls_cert = {:?}\ntls_key = {:?}\ntls_client_ca = {:?}\n",
cert_path.display().to_string(),
key_path.display().to_string(),
client_ca_path.display().to_string(),
))
.unwrap();
assert!(load(&config).unwrap().owp.is_some());
std::fs::remove_file(&cert_path).ok();
std::fs::remove_file(&key_path).ok();
std::fs::remove_file(&client_ca_path).ok();
}
#[test]
fn no_stomp_tls_configured_leaves_it_plaintext() {
let config: Config = toml::from_str("[stomp]\nenabled = true\n").unwrap();
assert!(load(&config).unwrap().stomp.is_none());
}
#[test]
fn a_disabled_stomp_adapter_is_not_checked_for_tls() {
let config: Config = toml::from_str(
"[stomp]\nenabled = false\ntls = true\ntls_ca = \"definitely/not/here.pem\"\n",
)
.unwrap();
assert!(load(&config).unwrap().stomp.is_none());
}
#[test]
fn stomp_tls_off_ignores_a_configured_ca() {
let config: Config = toml::from_str(
"[stomp]\nenabled = true\ntls = false\ntls_ca = \"definitely/not/here.pem\"\n",
)
.unwrap();
assert!(load(&config).unwrap().stomp.is_none());
}
#[test]
fn an_unreadable_stomp_ca_path_names_the_file() {
let config: Config = toml::from_str(
"[stomp]\nenabled = true\ntls = true\ntls_ca = \"definitely/not/here.pem\"\n",
)
.unwrap();
let err = load(&config).unwrap_err();
assert!(err.contains("stomp.tls_ca"), "{err}");
assert!(err.contains("definitely/not/here.pem"), "{err}");
}
#[test]
fn stomp_tls_server_name_defaults_to_the_broker_host_not_the_stomp_host_header() {
let config: Config = toml::from_str(
"[stomp]\nenabled = true\ntls = true\nbroker = \"not a hostname!:61612\"\nhost = \"/\"\n",
)
.unwrap();
let err = load(&config).unwrap_err();
assert!(err.contains("not a hostname!"), "{err}");
}
#[test]
fn an_explicit_stomp_server_name_overrides_the_broker_host() {
let config: Config = toml::from_str(
"[stomp]\nenabled = true\ntls = true\nbroker = \"127.0.0.1:61612\"\ntls_server_name = \"not a hostname!\"\n",
)
.unwrap();
let err = load(&config).unwrap_err();
assert!(err.contains("stomp.tls_server_name"), "{err}");
}
#[test]
fn a_stomp_client_cert_without_tls_is_refused_at_startup() {
let config: Config = toml::from_str(
"[stomp]\nenabled = true\ntls = false\ntls_client_cert = \"definitely/not/here.pem\"\n",
)
.unwrap();
let err = load(&config).unwrap_err();
assert!(err.contains("stomp.tls_client_cert"), "{err}");
assert!(err.contains("stomp.tls"), "{err}");
}
#[test]
fn a_stomp_client_key_without_tls_is_refused_at_startup() {
let config: Config = toml::from_str(
"[stomp]\nenabled = true\ntls = false\ntls_client_key = \"definitely/not/here.pem\"\n",
)
.unwrap();
let err = load(&config).unwrap_err();
assert!(err.contains("stomp.tls_client_key"), "{err}");
assert!(err.contains("stomp.tls"), "{err}");
}
#[test]
fn a_disabled_stomp_adapter_is_not_checked_for_a_client_cert() {
let config: Config = toml::from_str(
"[stomp]\nenabled = false\ntls_client_cert = \"definitely/not/here.pem\"\n",
)
.unwrap();
assert!(load(&config).unwrap().stomp.is_none());
}
#[test]
fn a_stomp_client_cert_and_key_load_with_tls_on() {
let rcgen::CertifiedKey { cert, key_pair } =
rcgen::generate_simple_self_signed(vec!["127.0.0.1".to_string()]).unwrap();
let dir = std::env::temp_dir().join("oa-gateway-stomp-mtls-test");
std::fs::create_dir_all(&dir).unwrap();
let cert_path = dir.join("client-cert.pem");
let key_path = dir.join("client-key.pem");
std::fs::write(&cert_path, cert.pem()).unwrap();
std::fs::write(&key_path, key_pair.serialize_pem()).unwrap();
let config: Config = toml::from_str(&format!(
"[stomp]\nenabled = true\ntls = true\nbroker = \"127.0.0.1:61612\"\ntls_client_cert = {:?}\ntls_client_key = {:?}\n",
cert_path.display().to_string(),
key_path.display().to_string(),
))
.unwrap();
assert!(load(&config).unwrap().stomp.is_some());
std::fs::remove_file(&cert_path).ok();
std::fs::remove_file(&key_path).ok();
}
}