server: Add a check for a changing private key

This checks that the private key used to encode the passwords has not
changed since last successful startup, leading to a corruption of all
the passwords. Lots of common scenario are covered, with various
combinations of key in a file or from a seed, set in the config file or
in an env variable or through CLI, and so on.
This commit is contained in:
Valentin Tolmer 2023-12-29 08:28:48 +01:00 committed by nitnelave
parent 997119cdcf
commit f2b1e73929
7 changed files with 524 additions and 33 deletions

3
Cargo.lock generated
View File

@ -1351,8 +1351,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e56602b469b2201400dec66a66aec5a9b8761ee97cd1b8c96ab2483fcc16cc9"
dependencies = [
"atomic",
"parking_lot",
"pear",
"serde",
"tempfile",
"toml",
"uncased",
"version_check",
@ -2473,6 +2475,7 @@ dependencies = [
"clap",
"cron",
"derive_builder",
"derive_more",
"figment",
"figment_file_provider_adapter",
"futures",

View File

@ -25,6 +25,7 @@ base64 = "0.21"
bincode = "1.3"
cron = "*"
derive_builder = "0.12"
derive_more = "0.99"
figment_file_provider_adapter = "0.1"
futures = "*"
futures-util = "*"
@ -162,3 +163,7 @@ features = ["file_locks"]
[dev-dependencies.uuid]
version = "1"
features = ["v4"]
[dev-dependencies.figment]
features = ["test"]
version = "*"

View File

@ -5,7 +5,8 @@ use crate::domain::{
use itertools::Itertools;
use sea_orm::{
sea_query::{
self, all, ColumnDef, Expr, ForeignKey, ForeignKeyAction, Func, Index, Query, Table, Value,
self, all, BlobSize::Blob, ColumnDef, Expr, ForeignKey, ForeignKeyAction, Func, Index,
Query, Table, Value,
},
ConnectionTrait, DatabaseTransaction, DbErr, DeriveIden, FromQueryResult, Iden, Order,
Statement, TransactionTrait,
@ -91,6 +92,8 @@ pub enum Metadata {
Table,
// Which version of the schema we're at.
Version,
PrivateKeyHash,
PrivateKeyLocation,
}
#[derive(FromQueryResult, PartialEq, Eq, Debug)]
@ -924,6 +927,28 @@ async fn migrate_to_v6(transaction: DatabaseTransaction) -> Result<DatabaseTrans
Ok(transaction)
}
async fn migrate_to_v7(transaction: DatabaseTransaction) -> Result<DatabaseTransaction, DbErr> {
let builder = transaction.get_database_backend();
transaction
.execute(
builder.build(
Table::alter()
.table(Metadata::Table)
.add_column(ColumnDef::new(Metadata::PrivateKeyHash).blob(Blob(Some(32)))),
),
)
.await?;
transaction
.execute(
builder.build(
Table::alter()
.table(Metadata::Table)
.add_column(ColumnDef::new(Metadata::PrivateKeyLocation).string_len(255)),
),
)
.await?;
Ok(transaction)
}
// This is needed to make an array of async functions.
macro_rules! to_sync {
($l:ident) => {
@ -950,6 +975,7 @@ pub async fn migrate_from_version(
to_sync!(migrate_to_v4),
to_sync!(migrate_to_v5),
to_sync!(migrate_to_v6),
to_sync!(migrate_to_v7),
];
assert_eq!(migrations.len(), (LAST_SCHEMA_VERSION.0 - 1) as usize);
for migration in 2..=last_version.0 {

View File

@ -1,12 +1,47 @@
use super::sql_migrations::{get_schema_version, migrate_from_version, upgrade_to_v1};
use sea_orm::{DeriveValueType, QueryResult, Value};
use crate::domain::sql_migrations::{
get_schema_version, migrate_from_version, upgrade_to_v1, Metadata,
};
use sea_orm::{
sea_query::Query, ConnectionTrait, DeriveValueType, Iden, QueryResult, TryGetable, Value,
};
use serde::{Deserialize, Serialize};
pub type DbConnection = sea_orm::DatabaseConnection;
#[derive(Copy, PartialEq, Eq, Debug, Clone, PartialOrd, Ord, DeriveValueType)]
pub struct SchemaVersion(pub i16);
pub const LAST_SCHEMA_VERSION: SchemaVersion = SchemaVersion(6);
pub const LAST_SCHEMA_VERSION: SchemaVersion = SchemaVersion(7);
#[derive(Copy, PartialEq, Eq, Debug, Clone, PartialOrd, Ord)]
pub struct PrivateKeyHash(pub [u8; 32]);
impl TryGetable for PrivateKeyHash {
fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result<Self, sea_orm::TryGetError> {
let index = format!("{pre}{col}");
Self::try_get_by(res, index.as_str())
}
fn try_get_by_index(res: &QueryResult, index: usize) -> Result<Self, sea_orm::TryGetError> {
Self::try_get_by(res, index)
}
fn try_get_by<I: sea_orm::ColIdx>(
res: &QueryResult,
index: I,
) -> Result<Self, sea_orm::TryGetError> {
Ok(PrivateKeyHash(
std::convert::TryInto::<[u8; 32]>::try_into(res.try_get_by::<Vec<u8>, I>(index)?)
.unwrap(),
))
}
}
impl From<PrivateKeyHash> for Value {
fn from(val: PrivateKeyHash) -> Self {
Self::from(val.0.to_vec())
}
}
pub async fn init_table(pool: &DbConnection) -> anyhow::Result<()> {
let version = {
@ -21,6 +56,71 @@ pub async fn init_table(pool: &DbConnection) -> anyhow::Result<()> {
Ok(())
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub enum ConfigLocation {
ConfigFile(String),
EnvironmentVariable(String),
CommandLine,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub enum PrivateKeyLocation {
KeySeed(ConfigLocation),
KeyFile(ConfigLocation, std::ffi::OsString),
Default,
#[cfg(test)]
Tests,
}
#[derive(Debug)]
pub struct PrivateKeyInfo {
pub private_key_hash: PrivateKeyHash,
pub private_key_location: PrivateKeyLocation,
}
pub async fn get_private_key_info(pool: &DbConnection) -> anyhow::Result<Option<PrivateKeyInfo>> {
let result = pool
.query_one(
pool.get_database_backend().build(
Query::select()
.column(Metadata::PrivateKeyHash)
.column(Metadata::PrivateKeyLocation)
.from(Metadata::Table),
),
)
.await?;
let result = match result {
None => return Ok(None),
Some(r) => r,
};
if let Ok(hash) = result.try_get("", &Metadata::PrivateKeyHash.to_string()) {
Ok(Some(PrivateKeyInfo {
private_key_hash: hash,
private_key_location: serde_json::from_str(
&result.try_get::<String>("", &Metadata::PrivateKeyLocation.to_string())?,
)?,
}))
} else {
Ok(None)
}
}
pub async fn set_private_key_info(pool: &DbConnection, info: PrivateKeyInfo) -> anyhow::Result<()> {
pool.execute(
pool.get_database_backend().build(
Query::update()
.table(Metadata::Table)
.value(Metadata::PrivateKeyHash, Value::from(info.private_key_hash))
.value(
Metadata::PrivateKeyLocation,
Value::from(serde_json::to_string(&info.private_key_location).unwrap()),
),
),
)
.await?;
Ok(())
}
#[cfg(test)]
mod tests {
use crate::domain::{

View File

@ -90,9 +90,13 @@ pub struct RunOpts {
pub database_url: Option<String>,
/// Force admin password reset to the config value.
#[clap(short, long, env = "LLDAP_FORCE_LADP_USER_PASS_RESET")]
#[clap(long, env = "LLDAP_FORCE_LADP_USER_PASS_RESET")]
pub force_ldap_user_pass_reset: Option<bool>,
/// Force update of the private key after a key change.
#[clap(long, env = "LLDAP_FORCE_UPDATE_PRIVATE_KEY")]
pub force_update_private_key: Option<bool>,
#[clap(flatten)]
pub smtp_opts: SmtpOpts,

View File

@ -1,12 +1,16 @@
use crate::{
domain::types::{AttributeName, UserId},
domain::{
sql_tables::{ConfigLocation, PrivateKeyHash, PrivateKeyInfo, PrivateKeyLocation},
types::{AttributeName, UserId},
},
infra::cli::{GeneralConfigOpts, LdapsOpts, RunOpts, SmtpEncryption, SmtpOpts, TestEmailOpts},
};
use anyhow::{Context, Result};
use anyhow::{bail, Context, Result};
use figment::{
providers::{Env, Format, Serialized, Toml},
Figment,
};
use figment_file_provider_adapter::FileAdapter;
use lettre::message::Mailbox;
use lldap_auth::opaque::{server::ServerSetup, KeyPair};
use secstr::SecUtf8;
@ -85,6 +89,8 @@ pub struct Configuration {
pub ldap_user_pass: SecUtf8,
#[builder(default = "false")]
pub force_ldap_user_pass_reset: bool,
#[builder(default = "false")]
pub force_update_private_key: bool,
#[builder(default = r#"String::from("sqlite://users.db?mode=rwc")"#)]
pub database_url: String,
#[builder(default)]
@ -107,7 +113,7 @@ pub struct Configuration {
pub http_url: Url,
#[serde(skip)]
#[builder(field(private), default = "None")]
server_setup: Option<ServerSetup>,
server_setup: Option<ServerSetupConfig>,
}
impl std::default::Default for Configuration {
@ -125,6 +131,7 @@ impl ConfigurationBuilder {
.and_then(|o| o.as_ref())
.map(SecUtf8::unsecure)
.unwrap_or_default(),
PrivateKeyLocation::Default,
)?;
Ok(self.server_setup(Some(server_setup)).private_build()?)
}
@ -133,20 +140,85 @@ impl ConfigurationBuilder {
pub fn for_tests() -> Configuration {
ConfigurationBuilder::default()
.verbose(true)
.server_setup(Some(generate_random_private_key()))
.server_setup(Some(ServerSetupConfig {
server_setup: generate_random_private_key(),
private_key_location: PrivateKeyLocation::Tests,
}))
.private_build()
.unwrap()
}
}
fn stable_hash(val: &[u8]) -> [u8; 32] {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(val);
hasher.finalize().into()
}
impl Configuration {
pub fn get_server_setup(&self) -> &ServerSetup {
self.server_setup.as_ref().unwrap()
&self.server_setup.as_ref().unwrap().server_setup
}
pub fn get_server_keys(&self) -> &KeyPair {
self.get_server_setup().keypair()
}
pub fn get_private_key_info(&self) -> PrivateKeyInfo {
PrivateKeyInfo {
private_key_hash: PrivateKeyHash(stable_hash(self.get_server_keys().private())),
private_key_location: self
.server_setup
.as_ref()
.unwrap()
.private_key_location
.clone(),
}
}
}
/// Returns whether the private key is entirely new.
pub fn compare_private_key_hashes(
previous_info: Option<&PrivateKeyInfo>,
private_key_info: &PrivateKeyInfo,
) -> Result<bool> {
match previous_info {
None => Ok(true),
Some(previous_info) => {
if previous_info.private_key_hash == private_key_info.private_key_hash {
Ok(false)
} else {
match (
&previous_info.private_key_location,
&private_key_info.private_key_location,
) {
(
PrivateKeyLocation::KeyFile(old_location, file_path),
PrivateKeyLocation::KeySeed(new_location),
) => {
bail!("The private key is configured to be generated from a seed (from {new_location:?}), but it used to come from the file {file_path:?} (defined in {old_location:?}). Did you just upgrade from <=v0.4 to >=v0.5? The key seed was not supported, revert to just using the file.");
}
(PrivateKeyLocation::Default, PrivateKeyLocation::KeySeed(new_location)) => {
bail!("The private key is configured to be generated from a seed (from {new_location:?}), but it used to come from default key file \"server_key\". Did you just upgrade from <=v0.4 to >=v0.5? The key seed was not yet supported, revert to just using the file.");
}
(
PrivateKeyLocation::KeyFile(old_location, old_path),
PrivateKeyLocation::KeyFile(new_location, new_path),
) => {
if old_path == new_path {
bail!("The contents of the private key file from {old_path:?} have changed. This usually means that the file was deleted and re-created. If using docker, make sure that the folder is made persistent (by mounting a volume or a directory). If you have several instances of LLDAP, make sure they share the same file (or switch to a key seed).");
} else {
bail!("The private key file used to be {old_path:?} (defined in {old_location:?}), but now is {new_path:?} (defined in {new_location:?}. Make sure to copy the old file in the new location.");
}
}
(old_location, new_location) => {
bail!("The private key has changed. It used to come from {old_location:?}, but now it comes from {new_location:?}.");
}
}
}
}
}
}
fn generate_random_private_key() -> ServerSetup {
@ -168,34 +240,129 @@ fn write_to_readonly_file(path: &std::path::Path, buffer: &[u8]) -> Result<()> {
Ok(file.write_all(buffer)?)
}
fn get_server_setup(file_path: &str, key_seed: &str) -> Result<ServerSetup> {
#[derive(Debug, Clone)]
pub struct ServerSetupConfig {
server_setup: ServerSetup,
private_key_location: PrivateKeyLocation,
}
#[derive(derive_more::From)]
enum PrivateKeyLocationOrFigment {
Figment(Figment),
PrivateKeyLocation(PrivateKeyLocation),
}
impl PrivateKeyLocationOrFigment {
fn for_key_seed(&self) -> PrivateKeyLocation {
match self {
PrivateKeyLocationOrFigment::Figment(config) => {
match config.find_metadata("key_seed") {
Some(figment::Metadata {
source: Some(figment::Source::File(path)),
..
}) => PrivateKeyLocation::KeySeed(ConfigLocation::ConfigFile(
path.to_string_lossy().to_string(),
)),
Some(figment::Metadata {
source: None, name, ..
}) => PrivateKeyLocation::KeySeed(ConfigLocation::EnvironmentVariable(
name.clone().to_string(),
)),
None
| Some(figment::Metadata {
source: Some(figment::Source::Code(_)),
..
}) => PrivateKeyLocation::Default,
other => panic!("Unexpected config location: {:?}", other),
}
}
PrivateKeyLocationOrFigment::PrivateKeyLocation(PrivateKeyLocation::KeyFile(
config_location,
_,
)) => {
panic!("Unexpected location: {:?}", config_location)
}
PrivateKeyLocationOrFigment::PrivateKeyLocation(location) => location.clone(),
}
}
fn for_key_file(&self, server_key_file: &str) -> PrivateKeyLocation {
match self {
PrivateKeyLocationOrFigment::Figment(config) => {
match config.find_metadata("key_file") {
Some(figment::Metadata {
source: Some(figment::Source::File(path)),
..
}) => PrivateKeyLocation::KeyFile(
ConfigLocation::ConfigFile(path.to_string_lossy().to_string()),
server_key_file.into(),
),
Some(figment::Metadata {
source: None, name, ..
}) => PrivateKeyLocation::KeyFile(
ConfigLocation::EnvironmentVariable(name.to_string()),
server_key_file.into(),
),
None
| Some(figment::Metadata {
source: Some(figment::Source::Code(_)),
..
}) => PrivateKeyLocation::Default,
other => panic!("Unexpected config location: {:?}", other),
}
}
PrivateKeyLocationOrFigment::PrivateKeyLocation(PrivateKeyLocation::KeySeed(file)) => {
panic!("Unexpected location: {:?}", file)
}
PrivateKeyLocationOrFigment::PrivateKeyLocation(location) => location.clone(),
}
}
}
fn get_server_setup<L: Into<PrivateKeyLocationOrFigment>>(
file_path: &str,
key_seed: &str,
private_key_location: L,
) -> Result<ServerSetupConfig> {
let private_key_location = private_key_location.into();
use std::fs::read;
let path = std::path::Path::new(file_path);
if !key_seed.is_empty() {
if file_path != "server_key" || path.exists() {
if path.exists() {
bail!(
"A key_seed was given, but a key file already exists at `{}`. Which one to use is ambiguous, aborting.\nNote: If you just migrated from <=v0.4 to >=v0.5, the previous version did not support key_seed, so it was falling back onto a key file. Remove the seed from the configuration.",
file_path
);
} else if file_path == "server_key" {
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");
println!("Generating the key from the key_seed");
}
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))
let mut rng = rand_chacha::ChaCha20Rng::from_seed(stable_hash(key_seed.as_bytes()));
Ok(ServerSetupConfig {
server_setup: ServerSetup::new(&mut rng),
private_key_location: private_key_location.for_key_seed(),
})
} else if path.exists() {
let bytes = read(file_path).context(format!("Could not read key file `{}`", file_path))?;
Ok(ServerSetup::deserialize(&bytes)?)
Ok(ServerSetupConfig {
server_setup: ServerSetup::deserialize(&bytes).context(format!(
"while parsing the contents of the `{}` file",
file_path
))?,
private_key_location: private_key_location.for_key_file(file_path),
})
} else {
let server_setup = generate_random_private_key();
write_to_readonly_file(path, &server_setup.serialize()).context(format!(
"Could not write the generated server setup to file `{}`",
file_path,
))?;
Ok(server_setup)
Ok(ServerSetupConfig {
server_setup,
private_key_location: private_key_location.for_key_file(file_path),
})
}
}
@ -250,6 +417,10 @@ impl ConfigOverrider for RunOpts {
if let Some(force_ldap_user_pass_reset) = self.force_ldap_user_pass_reset {
config.force_ldap_user_pass_reset = force_ldap_user_pass_reset;
}
if let Some(force_update_private_key) = self.force_update_private_key {
config.force_update_private_key = force_update_private_key;
}
self.smtp_opts.override_config(config);
self.ldaps_opts.override_config(config);
}
@ -323,21 +494,20 @@ pub fn init<C>(overrides: C) -> Result<Configuration>
where
C: TopLevelCommandOpts + ConfigOverrider,
{
let config_file = overrides.general_config().config_file.clone();
println!(
"Loading configuration from {}",
overrides.general_config().config_file
&overrides.general_config().config_file
);
use figment_file_provider_adapter::FileAdapter;
let ignore_keys = ["key_file", "cert_file"];
let mut config: Configuration = Figment::from(Serialized::defaults(
let figment_config = Figment::from(Serialized::defaults(
ConfigurationBuilder::default().private_build().unwrap(),
))
.merge(FileAdapter::wrap(Toml::file(config_file)).ignore(&ignore_keys))
.merge(FileAdapter::wrap(Env::prefixed("LLDAP_").split("__")).ignore(&ignore_keys))
.extract()?;
.merge(
FileAdapter::wrap(Toml::file(&overrides.general_config().config_file)).ignore(&ignore_keys),
)
.merge(FileAdapter::wrap(Env::prefixed("LLDAP_").split("__")).ignore(&ignore_keys));
let mut config: Configuration = figment_config.extract()?;
overrides.override_config(&mut config);
if config.verbose {
@ -350,6 +520,7 @@ where
.as_ref()
.map(SecUtf8::unsecure)
.unwrap_or_default(),
figment_config,
)?);
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.");
@ -366,12 +537,19 @@ where
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
use figment::Jail;
use pretty_assertions::assert_eq;
#[test]
fn check_generated_server_key() {
assert_eq!(
bincode::serialize(&get_server_setup("/doesnt/exist", "key seed").unwrap()).unwrap(),
bincode::serialize(
&get_server_setup("/doesnt/exist", "key seed", PrivateKeyLocation::Tests)
.unwrap()
.server_setup
)
.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,
@ -388,4 +566,153 @@ mod tests {
]
);
}
fn default_run_opts() -> RunOpts {
RunOpts::parse_from::<_, std::ffi::OsString>([])
}
fn write_random_key(jail: &Jail, file: &str) {
use std::io::Write;
let file = std::fs::File::create(jail.directory().join(file)).unwrap();
let mut writer = std::io::BufWriter::new(file);
writer
.write_all(&generate_random_private_key().serialize())
.unwrap();
}
#[test]
fn figment_location_extraction_key_file() {
Jail::expect_with(|jail| {
jail.create_file("lldap_config.toml", r#"key_file = "test""#)?;
jail.set_env("LLDAP_KEY_SEED", "a123");
let ignore_keys = ["key_file", "cert_file"];
let figment_config = Figment::from(Serialized::defaults(
ConfigurationBuilder::default().private_build().unwrap(),
))
.merge(FileAdapter::wrap(Toml::file("lldap_config.toml")).ignore(&ignore_keys))
.merge(FileAdapter::wrap(Env::prefixed("LLDAP_").split("__")).ignore(&ignore_keys));
assert_eq!(
PrivateKeyLocationOrFigment::Figment(figment_config).for_key_file("path"),
PrivateKeyLocation::KeyFile(
ConfigLocation::ConfigFile(
jail.directory()
.join("lldap_config.toml")
.to_string_lossy()
.to_string()
),
"path".into()
)
);
Ok(())
});
}
#[test]
fn check_server_setup_key_extraction_seed_success_with_nonexistant_file() {
Jail::expect_with(|jail| {
jail.create_file("lldap_config.toml", r#"key_file = "test""#)?;
jail.set_env("LLDAP_KEY_SEED", "a123");
init(default_run_opts()).unwrap();
Ok(())
});
}
#[test]
fn check_server_setup_key_extraction_seed_failure_with_existing_file() {
Jail::expect_with(|jail| {
jail.create_file("lldap_config.toml", r#"key_file = "test""#)?;
jail.set_env("LLDAP_KEY_SEED", "a123");
write_random_key(jail, "test");
init(default_run_opts()).unwrap_err();
Ok(())
});
}
#[test]
fn check_server_setup_key_extraction_file_success_with_existing_file() {
Jail::expect_with(|jail| {
jail.create_file("lldap_config.toml", r#"key_file = "test""#)?;
write_random_key(jail, "test");
init(default_run_opts()).unwrap();
Ok(())
});
}
#[test]
fn check_server_setup_key_extraction_file_success_with_nonexistent_file() {
Jail::expect_with(|jail| {
jail.create_file("lldap_config.toml", r#"key_file = "test""#)?;
init(default_run_opts()).unwrap();
Ok(())
});
}
#[test]
fn check_server_setup_key_extraction_file_with_previous_different_file() {
Jail::expect_with(|jail| {
jail.create_file("lldap_config.toml", r#"key_file = "test""#)?;
write_random_key(jail, "test");
let config = init(default_run_opts()).unwrap();
let info = config.get_private_key_info();
write_random_key(jail, "test");
let new_config = init(default_run_opts()).unwrap();
let error_message =
compare_private_key_hashes(Some(&info), &new_config.get_private_key_info())
.unwrap_err()
.to_string();
if let PrivateKeyLocation::KeyFile(_, file) = info.private_key_location {
assert!(
error_message.contains(
"The contents of the private key file from \"test\" have changed"
),
"{error_message}"
);
assert_eq!(file, "test");
} else {
panic!(
"Unexpected private key location: {:?}",
info.private_key_location
);
}
Ok(())
});
}
#[test]
fn check_server_setup_key_extraction_file_to_seed() {
Jail::expect_with(|jail| {
jail.create_file("lldap_config.toml", "")?;
write_random_key(jail, "server_key");
init(default_run_opts()).unwrap();
jail.create_file("lldap_config.toml", r#"key_seed = "test""#)?;
let error_message = init(default_run_opts()).unwrap_err().to_string();
assert!(
error_message.contains("A key_seed was given, but a key file already exists at",),
"{error_message}"
);
Ok(())
});
}
#[test]
fn check_server_setup_key_extraction_file_to_seed_removed_file() {
Jail::expect_with(|jail| {
jail.create_file("lldap_config.toml", "")?;
write_random_key(jail, "server_key");
let config = init(default_run_opts()).unwrap();
let info = config.get_private_key_info();
std::fs::remove_file(jail.directory().join("server_key")).unwrap();
jail.create_file("lldap_config.toml", r#"key_seed = "test""#)?;
let new_config = init(default_run_opts()).unwrap();
let error_message =
compare_private_key_hashes(Some(&info), &new_config.get_private_key_info())
.unwrap_err()
.to_string();
assert!(
error_message.contains("but it used to come from default key file",),
"{error_message}"
);
Ok(())
});
}
}

View File

@ -13,8 +13,14 @@ use crate::{
},
sql_backend_handler::SqlBackendHandler,
sql_opaque_handler::register_password,
sql_tables::{get_private_key_info, set_private_key_info},
},
infra::{
cli::*,
configuration::{compare_private_key_hashes, Configuration},
db_cleaner::Scheduler,
healthcheck, mail,
},
infra::{cli::*, configuration::Configuration, db_cleaner::Scheduler, healthcheck, mail},
};
use actix::Actor;
use actix_server::ServerBuilder;
@ -86,6 +92,26 @@ async fn set_up_server(config: Configuration) -> Result<ServerBuilder> {
domain::sql_tables::init_table(&sql_pool)
.await
.context("while creating the tables")?;
let private_key_info = config.get_private_key_info();
let force_update_private_key = config.force_update_private_key;
match (
compare_private_key_hashes(
get_private_key_info(&sql_pool).await?.as_ref(),
&private_key_info,
),
force_update_private_key,
) {
(Ok(false) | Err(_), true) => {
return Err(anyhow!("The private key has not changed, but force_update_private_key/LLDAP_FORCE_UPDATE_PRIVATE_KEY is set to true. Please set force_update_private_key to false and restart the server."));
}
(Ok(true), _) => {
set_private_key_info(&sql_pool, private_key_info).await?;
}
(Ok(false), false) => {}
(Err(e), false) => {
return Err(anyhow!("The private key encoding the passwords has changed since last successful startup. Changing the private key will invalidate all existing passwords. If you want to proceed, restart the server with the CLI arg --force_update_private_key or the env variable LLDAP_FORCE_UPDATE_PRIVATE_KEY=true. You probably also want --force_ldap_user_pass_reset / LLDAP_FORCE_LDAP_USER_PASS_RESET=true to reset the admin password to the value in the configuration.").context(e));
}
}
let backend_handler = SqlBackendHandler::new(config.clone(), sql_pool.clone());
ensure_group_exists(&backend_handler, "lldap_admin").await?;
ensure_group_exists(&backend_handler, "lldap_password_manager").await?;