From da364746c4b1f978c81db302556ced1a0537492b Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Wed, 12 Apr 2023 14:29:34 +0200 Subject: [PATCH] server: Derive the server key from a seed Fixes #504. --- Cargo.lock | 1 + lldap_config.docker_template.toml | 11 +++++- server/Cargo.toml | 1 + server/src/infra/cli.rs | 7 ++++ server/src/infra/configuration.rs | 64 +++++++++++++++++++++++++++---- 5 files changed, 75 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c10735c..75ea69a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2383,6 +2383,7 @@ dependencies = [ "opaque-ke", "orion", "rand 0.8.5", + "rand_chacha 0.3.1", "reqwest", "rustls", "rustls-pemfile", diff --git a/lldap_config.docker_template.toml b/lldap_config.docker_template.toml index a27f9d3..2cf002c 100644 --- a/lldap_config.docker_template.toml +++ b/lldap_config.docker_template.toml @@ -93,8 +93,15 @@ database_url = "sqlite:///data/users.db?mode=rwc" ## would still have to perform an (expensive) brute force attack to find ## each password. ## Randomly generated on first run if it doesn't exist. +## Alternatively, you can use key_seed to override this instead of relying on +## a file. key_file = "/data/private_key" +## Seed to generate the server private key, see key_file above. +## This can be any random string, the recommendation is that it's at least 12 +## characters long. +#key_seed = "RanD0m STR1ng" + ## Ignored attributes. ## Some services will request attributes that are not present in LLDAP. When it ## is the case, LLDAP will warn about the attribute being unknown. If you want @@ -106,7 +113,7 @@ key_file = "/data/private_key" ## Options to configure SMTP parameters, to send password reset emails. ## To set these options from environment variables, use the following format ## (example with "password"): LLDAP_SMTP_OPTIONS__PASSWORD -#[smtp_options] +[smtp_options] ## Whether to enabled password reset via email, from LLDAP. #enable_password_reset=true ## The SMTP server. @@ -128,7 +135,7 @@ key_file = "/data/private_key" ## Options to configure LDAPS. ## To set these options from environment variables, use the following format ## (example with "port"): LLDAP_LDAPS_OPTIONS__PORT -#[ldaps_options] +[ldaps_options] ## Whether to enable LDAPS. #enabled=true ## Port on which to listen. diff --git a/server/Cargo.toml b/server/Cargo.toml index 8e0a65f..68780ab 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -31,6 +31,7 @@ lber = "0.4.1" ldap3_proto = ">=0.3.1" log = "*" orion = "0.17" +rand_chacha = "0.3" rustls-pemfile = "1" serde = "*" serde_bytes = "0.11" diff --git a/server/src/infra/cli.rs b/server/src/infra/cli.rs index 0a3b94d..6625029 100644 --- a/server/src/infra/cli.rs +++ b/server/src/infra/cli.rs @@ -54,9 +54,16 @@ pub struct RunOpts { /// Path to the file that contains the private server key. /// It will be created if it doesn't exist. + /// Alternatively, you can set `server_key_seed`. If `server_key_seed` is given, + /// `server_key_file` will be ignored. #[clap(long, env = "LLDAP_SERVER_KEY_FILE")] pub server_key_file: Option, + /// Seed used to generate the private server key. + /// Takes precedence over `server_key_file`. + #[clap(long, env = "LLDAP_SERVER_KEY_SEED")] + pub server_key_seed: Option, + /// Change ldap host. Default: "0.0.0.0" #[clap(long, env = "LLDAP_LDAP_HOST")] pub ldap_host: Option, diff --git a/server/src/infra/configuration.rs b/server/src/infra/configuration.rs index b483a57..a99de87 100644 --- a/server/src/infra/configuration.rs +++ b/server/src/infra/configuration.rs @@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize}; pub struct MailOptions { #[builder(default = "false")] pub enable_password_reset: bool, - #[builder(default = "None")] + #[builder(default)] pub from: Option, #[builder(default = "None")] pub reply_to: Option, @@ -25,7 +25,7 @@ pub struct MailOptions { pub server: String, #[builder(default = "587")] pub port: u16, - #[builder(default = r#"String::default()"#)] + #[builder(default)] pub user: String, #[builder(default = r#"SecUtf8::from("")"#)] pub password: SecUtf8, @@ -78,7 +78,7 @@ pub struct Configuration { pub ldap_base_dn: String, #[builder(default = r#"UserId::new("admin")"#)] pub ldap_user_dn: UserId, - #[builder(default = r#"String::default()"#)] + #[builder(default)] pub ldap_user_email: String, #[builder(default = r#"SecUtf8::from("password")"#)] pub ldap_user_pass: SecUtf8, @@ -93,6 +93,8 @@ pub struct Configuration { #[builder(default = r#"String::from("server_key")"#)] pub key_file: String, #[builder(default)] + pub key_seed: String, + #[builder(default)] pub smtp_options: MailOptions, #[builder(default)] pub ldaps_options: LdapsOptions, @@ -111,7 +113,10 @@ impl std::default::Default for Configuration { impl ConfigurationBuilder { pub fn build(self) -> Result { - let server_setup = get_server_setup(self.key_file.as_deref().unwrap_or("server_key"))?; + let server_setup = get_server_setup( + self.key_file.as_deref().unwrap_or("server_key"), + self.key_seed.as_deref().unwrap_or_default(), + )?; Ok(self.server_setup(Some(server_setup)).private_build()?) } @@ -154,10 +159,25 @@ fn write_to_readonly_file(path: &std::path::Path, buffer: &[u8]) -> Result<()> { Ok(file.write_all(buffer)?) } -fn get_server_setup(file_path: &str) -> Result { +fn get_server_setup(file_path: &str, key_seed: &str) -> Result { use std::fs::read; let path = std::path::Path::new(file_path); - if path.exists() { + if !key_seed.is_empty() { + if file_path != "server_key" || path.exists() { + eprintln!("WARNING: A key_seed was given, we will ignore the server_key and generate one from the seed!"); + } else { + println!("Got a key_seed, ignoring key_file"); + } + let hash = |val: &[u8]| -> [u8; 32] { + use sha2::{Digest, Sha256}; + let mut seed_hasher = Sha256::new(); + seed_hasher.update(val); + seed_hasher.finalize().into() + }; + use rand::SeedableRng; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(hash(key_seed.as_bytes())); + Ok(ServerSetup::new(&mut rng)) + } else if path.exists() { let bytes = read(file_path).context(format!("Could not read key file `{}`", file_path))?; Ok(ServerSetup::deserialize(&bytes)?) } else { @@ -198,6 +218,10 @@ impl ConfigOverrider for RunOpts { config.key_file = path.to_string(); } + if let Some(seed) = self.server_key_seed.as_ref() { + config.key_seed = seed.to_string(); + } + if let Some(port) = self.ldap_port { config.ldap_port = port; } @@ -306,7 +330,7 @@ where if config.verbose { println!("Configuration: {:#?}", &config); } - config.server_setup = Some(get_server_setup(&config.key_file)?); + config.server_setup = Some(get_server_setup(&config.key_file, &config.key_seed)?); if config.jwt_secret == SecUtf8::from("secretjwtsecret") { println!("WARNING: Default JWT secret used! This is highly unsafe and can allow attackers to log in as admin."); } @@ -318,3 +342,29 @@ where } Ok(config) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_generated_server_key() { + assert_eq!( + bincode::serialize(&get_server_setup("/doesnt/exist", "key seed").unwrap()).unwrap(), + [ + 255, 206, 202, 50, 247, 13, 59, 191, 69, 244, 148, 187, 150, 227, 12, 250, 20, 207, + 211, 151, 147, 33, 107, 132, 2, 252, 121, 94, 97, 6, 97, 232, 163, 168, 86, 246, + 249, 186, 31, 204, 59, 75, 65, 134, 108, 159, 15, 70, 246, 250, 150, 195, 54, 197, + 195, 176, 150, 200, 157, 119, 13, 173, 119, 8, 32, 0, 0, 0, 0, 0, 0, 0, 248, 123, + 35, 91, 194, 51, 52, 57, 191, 210, 68, 227, 107, 166, 232, 37, 195, 244, 100, 84, + 88, 212, 190, 12, 195, 57, 83, 72, 127, 189, 179, 16, 32, 0, 0, 0, 0, 0, 0, 0, 128, + 112, 60, 207, 205, 69, 67, 73, 24, 175, 187, 62, 16, 45, 59, 136, 78, 40, 187, 54, + 159, 94, 116, 33, 133, 119, 231, 43, 199, 164, 141, 7, 32, 0, 0, 0, 0, 0, 0, 0, + 212, 134, 53, 203, 131, 24, 138, 211, 162, 28, 23, 233, 251, 82, 34, 66, 98, 12, + 249, 205, 35, 208, 241, 50, 128, 131, 46, 189, 211, 51, 56, 109, 32, 0, 0, 0, 0, 0, + 0, 0, 84, 20, 147, 25, 50, 5, 243, 203, 216, 180, 175, 121, 159, 96, 123, 183, 146, + 251, 22, 44, 98, 168, 67, 224, 255, 139, 159, 25, 24, 254, 88, 3 + ] + ); + } +}