Skip to content

Commit 8231b84

Browse files
author
tnull
committed
f - Tighten entropy source configuration
Review feedback preferred a single explicit entropy source over two mutually exclusive Options. Convert the config to an enum and add CLI and environment overrides so non-TOML deployments can select the entropy path. Require legacy raw seed users to opt in via seed_file so default startup consistently uses the mnemonic path. Set permissions before the final sync so file contents and permission metadata are flushed together. Co-Authored-By: HAL 9000
1 parent 1aaac92 commit 8231b84

2 files changed

Lines changed: 140 additions & 55 deletions

File tree

ldk-server/src/util/config.rs

Lines changed: 126 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,14 @@ pub struct Config {
6969

7070
/// Configuration for the node's entropy source.
7171
///
72-
/// When both `mnemonic_file` and `seed_file` are unset, the node defaults to loading or
73-
/// generating a BIP39 mnemonic at `<storage_dir>/keys_mnemonic`. If a legacy raw-seed file
74-
/// exists at `<storage_dir>/keys_seed`, it is used for backwards compatibility.
72+
/// When unset, the node defaults to loading or generating a BIP39 mnemonic at
73+
/// `<storage_dir>/keys_mnemonic`.
7574
#[derive(Debug, Default, Clone, PartialEq, Eq)]
76-
pub struct EntropyConfig {
77-
pub mnemonic_file: Option<String>,
78-
pub seed_file: Option<String>,
75+
pub enum EntropyConfig {
76+
#[default]
77+
Default,
78+
MnemonicFile(String),
79+
SeedFile(String),
7980
}
8081

8182
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -135,7 +136,7 @@ struct ConfigBuilder {
135136
}
136137

137138
impl ConfigBuilder {
138-
fn merge_toml(&mut self, toml: TomlConfig) {
139+
fn merge_toml(&mut self, toml: TomlConfig) -> io::Result<()> {
139140
if let Some(node) = toml.node {
140141
self.network = node.network.or(self.network);
141142
self.listening_addresses =
@@ -151,9 +152,7 @@ impl ConfigBuilder {
151152
node.async_payments_role.or(self.async_payments_role.clone());
152153
self.rgs_server_url = node.rgs_server_url.or(self.rgs_server_url.clone());
153154
if let Some(entropy) = node.entropy {
154-
self.entropy.mnemonic_file =
155-
entropy.mnemonic_file.or(self.entropy.mnemonic_file.take());
156-
self.entropy.seed_file = entropy.seed_file.or(self.entropy.seed_file.take());
155+
self.entropy = EntropyConfig::from_paths(entropy.mnemonic_file, entropy.seed_file)?;
157156
}
158157
}
159158

@@ -209,9 +208,11 @@ impl ConfigBuilder {
209208
if let Some(hrn) = toml.hrn {
210209
self.hrn = Some(hrn);
211210
}
211+
212+
Ok(())
212213
}
213214

214-
fn merge_args(&mut self, args: &ArgsConfig) {
215+
fn merge_args(&mut self, args: &ArgsConfig) -> io::Result<()> {
215216
if let Some(network) = args.node_network {
216217
self.network = Some(network);
217218
}
@@ -275,6 +276,15 @@ impl ConfigBuilder {
275276
if let Some(tor_proxy_address) = &args.tor_proxy_address {
276277
self.tor_proxy_address = Some(tor_proxy_address.clone());
277278
}
279+
280+
if args.node_entropy_mnemonic_file.is_some() || args.node_entropy_seed_file.is_some() {
281+
self.entropy = EntropyConfig::from_paths(
282+
args.node_entropy_mnemonic_file.clone(),
283+
args.node_entropy_seed_file.clone(),
284+
)?;
285+
}
286+
287+
Ok(())
278288
}
279289

280290
fn build(self) -> io::Result<Config> {
@@ -450,13 +460,6 @@ impl ConfigBuilder {
450460
None => HumanReadableNamesConfig::default(),
451461
};
452462

453-
if self.entropy.mnemonic_file.is_some() && self.entropy.seed_file.is_some() {
454-
return Err(io::Error::new(
455-
io::ErrorKind::InvalidInput,
456-
"Only one of `node.entropy.mnemonic_file` and `node.entropy.seed_file` may be configured.".to_string(),
457-
));
458-
}
459-
460463
Ok(Config {
461464
network,
462465
listening_addrs,
@@ -484,6 +487,20 @@ impl ConfigBuilder {
484487
}
485488
}
486489

490+
impl EntropyConfig {
491+
fn from_paths(mnemonic_file: Option<String>, seed_file: Option<String>) -> io::Result<Self> {
492+
match (mnemonic_file, seed_file) {
493+
(Some(_), Some(_)) => Err(io::Error::new(
494+
io::ErrorKind::InvalidInput,
495+
"Only one of `node.entropy.mnemonic_file`/`--node-entropy-mnemonic-file` and `node.entropy.seed_file`/`--node-entropy-seed-file` may be configured.".to_string(),
496+
)),
497+
(Some(mnemonic_file), None) => Ok(Self::MnemonicFile(mnemonic_file)),
498+
(None, Some(seed_file)) => Ok(Self::SeedFile(seed_file)),
499+
(None, None) => Ok(Self::Default),
500+
}
501+
}
502+
}
503+
487504
/// Configuration loaded from a TOML file.
488505
#[derive(Deserialize, Serialize)]
489506
#[serde(deny_unknown_fields)]
@@ -861,6 +878,20 @@ pub struct ArgsConfig {
861878
)]
862879
node_async_payments_role: Option<String>,
863880

881+
#[arg(
882+
long,
883+
env = "LDK_SERVER_NODE_ENTROPY_MNEMONIC_FILE",
884+
help = "Path to the BIP39 mnemonic file used for node entropy."
885+
)]
886+
node_entropy_mnemonic_file: Option<String>,
887+
888+
#[arg(
889+
long,
890+
env = "LDK_SERVER_NODE_ENTROPY_SEED_FILE",
891+
help = "Legacy path to a raw 64-byte seed file used for node entropy."
892+
)]
893+
node_entropy_seed_file: Option<String>,
894+
864895
#[arg(
865896
long,
866897
env = "LDK_SERVER_METRICS_ENABLED",
@@ -918,10 +949,10 @@ pub fn load_config(args: &ArgsConfig) -> io::Result<Config> {
918949
)
919950
})?;
920951

921-
builder.merge_toml(toml_config);
952+
builder.merge_toml(toml_config)?;
922953
}
923954

924-
builder.merge_args(args);
955+
builder.merge_args(args)?;
925956

926957
builder.build()
927958
}
@@ -961,8 +992,10 @@ fn parse_host_port(addr: &str) -> io::Result<(String, u16)> {
961992

962993
#[cfg(test)]
963994
mod tests {
995+
use std::ffi::OsString;
964996
use std::str::FromStr;
965997

998+
use clap::Parser;
966999
use ldk_node::bitcoin::secp256k1::PublicKey;
9671000
use ldk_node::bitcoin::Network;
9681001
use ldk_node::lightning::ln::msgs::SocketAddress;
@@ -1036,6 +1069,8 @@ mod tests {
10361069
metrics_username: None,
10371070
metrics_password: None,
10381071
tor_proxy_address: None,
1072+
node_entropy_mnemonic_file: None,
1073+
node_entropy_seed_file: None,
10391074
}
10401075
}
10411076

@@ -1058,6 +1093,8 @@ mod tests {
10581093
metrics_username: None,
10591094
metrics_password: None,
10601095
tor_proxy_address: None,
1096+
node_entropy_mnemonic_file: None,
1097+
node_entropy_seed_file: None,
10611098
}
10621099
}
10631100

@@ -1068,6 +1105,13 @@ mod tests {
10681105
)
10691106
}
10701107

1108+
fn restore_env_var(key: &str, old_value: Option<OsString>) {
1109+
match old_value {
1110+
Some(value) => std::env::set_var(key, value),
1111+
None => std::env::remove_var(key),
1112+
}
1113+
}
1114+
10711115
#[test]
10721116
fn test_config_from_file() {
10731117
let storage_path = std::env::temp_dir();
@@ -1923,8 +1967,10 @@ mod tests {
19231967
Some(storage_path.join(config_file_name).to_string_lossy().to_string());
19241968

19251969
let config = load_config(&args_config).unwrap();
1926-
assert_eq!(config.entropy.mnemonic_file, Some("/some/path/keys_mnemonic".to_string()));
1927-
assert_eq!(config.entropy.seed_file, None);
1970+
assert_eq!(
1971+
config.entropy,
1972+
EntropyConfig::MnemonicFile("/some/path/keys_mnemonic".to_string())
1973+
);
19281974
}
19291975

19301976
#[test]
@@ -1956,4 +2002,62 @@ mod tests {
19562002
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
19572003
assert!(err.to_string().contains("Only one of"));
19582004
}
2005+
2006+
#[test]
2007+
fn test_node_entropy_args_override_file() {
2008+
let storage_path = std::env::temp_dir();
2009+
let config_file_name = "test_node_entropy_args_override_file.toml";
2010+
2011+
let toml_config = r#"
2012+
[node]
2013+
network = "regtest"
2014+
grpc_service_address = "127.0.0.1:3002"
2015+
2016+
[node.entropy]
2017+
mnemonic_file = "/config/keys_mnemonic"
2018+
2019+
[bitcoind]
2020+
rpc_address = "127.0.0.1:8332"
2021+
rpc_user = "bitcoind-testuser"
2022+
rpc_password = "bitcoind-testpassword"
2023+
"#;
2024+
2025+
fs::write(storage_path.join(config_file_name), toml_config).unwrap();
2026+
let mut args_config = empty_args_config();
2027+
args_config.config_file =
2028+
Some(storage_path.join(config_file_name).to_string_lossy().to_string());
2029+
args_config.node_entropy_seed_file = Some("/legacy/keys_seed".to_string());
2030+
2031+
let config = load_config(&args_config).unwrap();
2032+
assert_eq!(config.entropy, EntropyConfig::SeedFile("/legacy/keys_seed".to_string()));
2033+
}
2034+
2035+
#[test]
2036+
fn test_rejects_both_entropy_args() {
2037+
let mut args_config = default_args_config();
2038+
args_config.node_entropy_mnemonic_file = Some("/some/path/keys_mnemonic".to_string());
2039+
args_config.node_entropy_seed_file = Some("/some/path/keys_seed".to_string());
2040+
2041+
let err = load_config(&args_config).unwrap_err();
2042+
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
2043+
assert!(err.to_string().contains("Only one of"));
2044+
}
2045+
2046+
#[test]
2047+
fn test_node_entropy_mnemonic_file_env() {
2048+
let mnemonic_key = "LDK_SERVER_NODE_ENTROPY_MNEMONIC_FILE";
2049+
let seed_key = "LDK_SERVER_NODE_ENTROPY_SEED_FILE";
2050+
let old_mnemonic = std::env::var_os(mnemonic_key);
2051+
let old_seed = std::env::var_os(seed_key);
2052+
2053+
std::env::set_var(mnemonic_key, "/env/keys_mnemonic");
2054+
std::env::remove_var(seed_key);
2055+
let parse_result = ArgsConfig::try_parse_from(["ldk-server"]);
2056+
restore_env_var(mnemonic_key, old_mnemonic);
2057+
restore_env_var(seed_key, old_seed);
2058+
2059+
let args_config = parse_result.unwrap();
2060+
assert_eq!(args_config.node_entropy_mnemonic_file, Some("/env/keys_mnemonic".to_string()));
2061+
assert_eq!(args_config.node_entropy_seed_file, None);
2062+
}
19592063
}

ldk-server/src/util/entropy.rs

Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,20 @@ use log::info;
2020
use crate::util::config::EntropyConfig;
2121

2222
const DEFAULT_MNEMONIC_FILE: &str = "keys_mnemonic";
23+
#[cfg(test)]
2324
const LEGACY_SEED_FILE: &str = "keys_seed";
2425

2526
pub(crate) fn load_or_generate_node_entropy(
2627
storage_dir: &Path, entropy_config: &EntropyConfig,
2728
) -> io::Result<NodeEntropy> {
28-
if let Some(seed_file) = &entropy_config.seed_file {
29-
info!("Loading node entropy from raw seed file at {}", seed_file);
30-
return NodeEntropy::from_seed_path(seed_file.clone())
31-
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()));
32-
}
33-
34-
let legacy_seed_path = storage_dir.join(LEGACY_SEED_FILE);
35-
if entropy_config.mnemonic_file.is_none() && legacy_seed_path.exists() {
36-
info!(
37-
"Detected legacy raw seed file at {}; continuing to use it. New installs use a BIP39 mnemonic by default.",
38-
legacy_seed_path.display()
39-
);
40-
return NodeEntropy::from_seed_path(legacy_seed_path.to_string_lossy().into_owned())
41-
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()));
42-
}
43-
44-
let mnemonic_path = match &entropy_config.mnemonic_file {
45-
Some(p) => PathBuf::from(p),
46-
None => storage_dir.join(DEFAULT_MNEMONIC_FILE),
29+
let mnemonic_path = match entropy_config {
30+
EntropyConfig::SeedFile(seed_file) => {
31+
info!("Loading node entropy from raw seed file at {}", seed_file);
32+
return NodeEntropy::from_seed_path(seed_file.clone())
33+
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()));
34+
},
35+
EntropyConfig::MnemonicFile(path) => PathBuf::from(path),
36+
EntropyConfig::Default => storage_dir.join(DEFAULT_MNEMONIC_FILE),
4737
};
4838

4939
let mnemonic = if mnemonic_path.exists() {
@@ -72,9 +62,9 @@ pub(crate) fn load_or_generate_node_entropy(
7262

7363
fn write_mnemonic_file(path: &Path, mnemonic: &Mnemonic) -> io::Result<()> {
7464
let mut f = fs::OpenOptions::new().create_new(true).write(true).mode(0o600).open(path)?;
65+
f.set_permissions(fs::Permissions::from_mode(0o600))?;
7566
writeln!(f, "{}", mnemonic)?;
7667
f.sync_all()?;
77-
fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
7868
Ok(())
7969
}
8070

@@ -133,17 +123,14 @@ mod tests {
133123
}
134124

135125
#[test]
136-
fn auto_detects_legacy_keys_seed() {
126+
fn default_entropy_ignores_legacy_keys_seed() {
137127
let dir = tempdir("legacy");
138128
let legacy_path = dir.join(LEGACY_SEED_FILE);
139129
fs::write(&legacy_path, vec![0x42u8; 64]).unwrap();
140130

141131
load_or_generate_node_entropy(&dir, &EntropyConfig::default()).unwrap();
142132

143-
assert!(
144-
!dir.join(DEFAULT_MNEMONIC_FILE).exists(),
145-
"keys_mnemonic was unexpectedly created"
146-
);
133+
assert!(dir.join(DEFAULT_MNEMONIC_FILE).exists(), "keys_mnemonic was not created");
147134
assert!(legacy_path.exists(), "legacy keys_seed was removed");
148135
}
149136

@@ -153,10 +140,7 @@ mod tests {
153140
let custom_seed = dir.join("custom-seed.bin");
154141
fs::write(&custom_seed, vec![0x17u8; 64]).unwrap();
155142

156-
let cfg = EntropyConfig {
157-
seed_file: Some(custom_seed.to_string_lossy().into_owned()),
158-
mnemonic_file: None,
159-
};
143+
let cfg = EntropyConfig::SeedFile(custom_seed.to_string_lossy().into_owned());
160144

161145
load_or_generate_node_entropy(&dir, &cfg).unwrap();
162146

@@ -183,10 +167,7 @@ mod tests {
183167
fn custom_mnemonic_path_respected() {
184168
let dir = tempdir("custom-mnemonic");
185169
let custom_path = dir.join("elsewhere").join("my_mnemonic");
186-
let cfg = EntropyConfig {
187-
mnemonic_file: Some(custom_path.to_string_lossy().into_owned()),
188-
seed_file: None,
189-
};
170+
let cfg = EntropyConfig::MnemonicFile(custom_path.to_string_lossy().into_owned());
190171

191172
load_or_generate_node_entropy(&dir, &cfg).unwrap();
192173

0 commit comments

Comments
 (0)