use std::{
num::{NonZeroU16, NonZeroU64},
path::PathBuf,
};
use sequoia_openpgp::crypto::Password;
use serde::{Deserialize, Serialize};
use crate::config::Credentials;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
#[serde(default = "default_state_directory")]
pub state_directory: PathBuf,
#[serde(default = "default_socket_path")]
pub signer_socket_path: PathBuf,
pub bridge_hostname: String,
pub bridge_port: u16,
pub connection_pool_size: usize,
#[serde(default = "default_idle_client_timeout")]
pub idle_client_timeout: NonZeroU64,
#[serde(default = "default_connection_watchdog_timeout")]
pub connection_watchdog_timeout: NonZeroU64,
pub user_password_length: NonZeroU16,
pub credentials: Credentials,
pub certificate_subject: X509SubjectName,
pub openpgp_user_id: String,
#[serde(default)]
pub pkcs11_bindings: Vec<Pkcs11Binding>,
}
const fn default_idle_client_timeout() -> NonZeroU64 {
NonZeroU64::new(60 * 60).expect("set a non-zero default")
}
const fn default_connection_watchdog_timeout() -> NonZeroU64 {
NonZeroU64::new(3 * 60 * 60).expect("set a non-zero default")
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct X509SubjectName {
pub country: String,
pub state_or_province: String,
pub locality: String,
pub organization: String,
pub organizational_unit: String,
}
impl Default for X509SubjectName {
fn default() -> Self {
Self {
country: "US".to_string(),
state_or_province: "Massachusetts".to_string(),
locality: "Cambridge".to_string(),
organization: "An Example Organization".to_string(),
organizational_unit: "Example Department of the Organization".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct Pkcs11Binding {
pub certificate: PathBuf,
pub private_key: Option<String>,
#[doc(hidden)]
#[serde(skip)]
pub pin: Option<Password>,
}
impl Pkcs11Binding {
pub(crate) fn can_unbind(&self) -> bool {
self.private_key.is_some() && self.pin.is_some()
}
}
impl Config {
pub fn database(&self) -> PathBuf {
self.state_directory.join("siguldry.sqlite")
}
}
impl Default for Config {
fn default() -> Self {
Self {
state_directory: default_state_directory(),
signer_socket_path: default_socket_path(),
idle_client_timeout: default_idle_client_timeout(),
connection_watchdog_timeout: default_connection_watchdog_timeout(),
bridge_hostname: "bridge.example.com".to_string(),
bridge_port: 44333,
connection_pool_size: 32,
user_password_length: NonZeroU16::new(32).unwrap(),
credentials: Credentials {
private_key: PathBuf::from("siguldry.server.private_key.pem"),
certificate: PathBuf::from("siguldry.server.certificate.pem"),
ca_certificate: PathBuf::from("siguldry.ca_certificate.pem"),
},
pkcs11_bindings: vec![],
certificate_subject: Default::default(),
openpgp_user_id: "Test Signing <sign@example.com>".to_string(),
}
}
}
impl std::fmt::Display for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
toml::ser::to_string_pretty(&self).unwrap_or_default()
)
}
}
fn default_socket_path() -> PathBuf {
PathBuf::from("/run/siguldry-signer/signer.socket")
}
fn default_state_directory() -> PathBuf {
PathBuf::from("/var/lib/siguldry/")
}
#[cfg(test)]
mod tests {
#[test]
fn load_example_config() -> anyhow::Result<()> {
let example_conf_path =
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("server.toml.example");
let example_conf = std::fs::read_to_string(&example_conf_path)?;
toml::de::from_str::<super::Config>(&example_conf)?;
Ok(())
}
#[test]
fn pkcs11_bindings_extra_key() -> anyhow::Result<()> {
let config = r#"
certificate = "cert.pem"
private_key = "pkcs11:token"
other_key = 42
"#;
if let Err(error) = toml::from_str::<super::Pkcs11Binding>(config) {
assert_eq!(
error.message(),
"unknown field `other_key`, expected `certificate` or `private_key`"
);
} else {
panic!("Config should fail to load");
}
Ok(())
}
#[test]
fn x509_subject_name_extra_key() -> anyhow::Result<()> {
let config = r#"
other_key = 42
country = "US"
state_or_province = "Maryland"
locality = "Bethesda"
organization = "Cat Caretaker"
organizational_unit = "Primary Cat Scratcher"
"#;
if let Err(error) = toml::from_str::<super::X509SubjectName>(config) {
assert_eq!(
error.message(),
"unknown field `other_key`, expected one of \
`country`, `state_or_province`, `locality`, `organization`, `organizational_unit`"
);
} else {
panic!("Config should fail to load");
}
Ok(())
}
#[test]
fn config_extra_key() -> anyhow::Result<()> {
let config = r#"
state_directory = "/var/lib/siguldry/"
bridge_hostname = "bridge.example.com"
bridge_port = 44333
connection_pool_size = 16
user_password_length = 64
openpgp_user_id = "Fedora <fedora-openpgp@fedoraproject.org>"
another_key = 42
pkcs11_bindings = []
[credentials]
private_key = "siguldry.server.private_key.pem"
certificate = "/etc/siguldry/server.cert"
ca_certificate = "/etc/siguldry/ca.crt"
[certificate_subject]
country = "US"
state_or_province = "Maryland"
locality = "Bethesda"
organization = "Cat Caretaker"
organizational_unit = "Primary Cat Scratcher"
"#;
if let Err(error) = toml::from_str::<super::Config>(config) {
assert!(error.message().contains("unknown field `another_key`"));
} else {
panic!("Config should fail to load");
}
Ok(())
}
}