From b90cfd1fde4e1c00d03e44976753d0044b273bda Mon Sep 17 00:00:00 2001 From: tv Date: Tue, 27 Oct 2026 00:58:45 +0100 Subject: introduce node keys --- src/config/mod.rs | 67 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 12 deletions(-) (limited to 'src/config/mod.rs') diff --git a/src/config/mod.rs b/src/config/mod.rs index 928730e..65d481d 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -179,6 +179,20 @@ pub(crate) const OPTIONS: &[OptionSpec] = &[ None, "File where this node remembers its swarm across restarts: the swarm ID and some members to rejoin through (not the document). Written once ready, then rarely. Without it, a restarted node knows only its configured seeds.", ), + option( + "node-key-file", + Kind::Text, + "PATH", + None, + "This node's key (Ed25519, 32 raw bytes): who the node is, whatever transport reaches it. Keep it with the node: a new key is a new node. Required.", + ), + option( + "create-node-key", + Kind::Flag, + "BOOL", + Some("false"), + "Create a new node key at node-key-file if there is no file there (a new node). Without it, a missing key file is an error.", + ), option( "swarm-key-file", Kind::Text, @@ -311,6 +325,9 @@ pub(crate) struct Settings { pub(crate) remake: Option, #[serde(default)] pub(crate) force: Vec, + pub(crate) node_key_file: PathBuf, + #[serde(default)] + pub(crate) create_node_key: bool, pub(crate) swarm_key_file: Option, pub(crate) alias: Option, #[serde(default = "default_keepalive", with = "crate::units::duration_option")] @@ -788,6 +805,7 @@ fn culprit(merged: &Layer) -> Option<(&String, &Entry)> { } let mut alone = serde_json::Map::new(); alone.insert("htpasswd".to_owned(), JsonValue::String(String::new())); + alone.insert("node-key-file".to_owned(), JsonValue::String(String::new())); alone.insert((*name).clone(), entry.value.clone()); serde_json::from_value::(JsonValue::Object(alone)).is_err() }) @@ -1122,7 +1140,7 @@ mod tests { written: None, }, }; - let mut merged = layer(json!({ "htpasswd": "u" })); + let mut merged = layer(json!({ "htpasswd": "u", "node-key-file": "node.key" })); merged.insert( "force".to_owned(), from(Provenance::File(PathBuf::from("continuum.toml"))), @@ -1135,7 +1153,11 @@ mod tests { #[test] fn options_and_settings_agree() { // Every option is a Settings field, and every field an option. - let loaded = resolve(layer(json!({ "htpasswd": "users" })), Vec::new()).unwrap(); + let loaded = resolve( + layer(json!({ "htpasswd": "users", "node-key-file": "node.key" })), + Vec::new(), + ) + .unwrap(); let JsonValue::Object(fields) = serde_json::to_value(&loaded.settings).unwrap() else { panic!() }; @@ -1182,6 +1204,12 @@ mod tests { file(json!("u")), Overlay::FileOverFile, ); + overlay( + &mut merged, + "node-key-file".into(), + file(json!("node.key")), + Overlay::FileOverFile, + ); overlay( &mut merged, "mycelium-key-file".into(), @@ -1230,7 +1258,9 @@ mod tests { #[test] fn report_hides_imported_values() { - let mut values = layer(json!({ "htpasswd": "users", "alias": "secret-ish" })); + let mut values = layer( + json!({ "htpasswd": "users", "node-key-file": "node.key", "alias": "secret-ish" }), + ); values.get_mut("alias").unwrap().origin.imports = vec!["env:HOSTNAME".into()]; let report = resolve(values, Vec::new()) .unwrap() @@ -1265,9 +1295,12 @@ mod tests { /// `Settings::check` and the override rule name the same supervised-only options. #[test] fn supervised_only_options_are_the_conflicts() { - let settings = resolve(layer(json!({ "htpasswd": "u" })), Vec::new()) - .unwrap() - .settings; + let settings = resolve( + layer(json!({ "htpasswd": "u", "node-key-file": "node.key" })), + Vec::new(), + ) + .unwrap() + .settings; let names: Vec<&str> = settings .supervised_only() .iter() @@ -1283,14 +1316,14 @@ mod tests { assert!(resolve(layer(json!({})), Vec::new()).is_err()); // htpasswd is required assert!( resolve( - layer(json!({ "htpasswd": "u", "neighbours": 1 })), + layer(json!({ "htpasswd": "u", "node-key-file": "node.key", "neighbours": 1 })), Vec::new() ) .is_err() ); assert!( resolve( - layer(json!({ "htpasswd": "u", "debug-epoch": 3 })), + layer(json!({ "htpasswd": "u", "node-key-file": "node.key", "debug-epoch": 3 })), Vec::new() ) .is_err() @@ -1299,6 +1332,7 @@ mod tests { resolve( layer(json!({ "htpasswd": "u", + "node-key-file": "node.key", "mycelium-rest-url": "http://[::1]:8989", "mycelium-key-file": "k", })), @@ -1306,10 +1340,16 @@ mod tests { ) .is_err() ); - assert!(resolve(layer(json!({ "htpasswd": "u", "alias": " " })), Vec::new()).is_err()); assert!( resolve( - layer(json!({ "htpasswd": "u", "keepalive": "x" })), + layer(json!({ "htpasswd": "u", "node-key-file": "node.key", "alias": " " })), + Vec::new() + ) + .is_err() + ); + assert!( + resolve( + layer(json!({ "htpasswd": "u", "node-key-file": "node.key", "keepalive": "x" })), Vec::new() ) .is_err() @@ -1352,6 +1392,8 @@ mod tests { "continuum", "--htpasswd", "users", + "--node-key-file", + "node.key", "--alias", "node-${env:CONTINUUM_TEST_SITE}", "--seeds", @@ -1397,8 +1439,9 @@ mod tests { #[test] fn secrets_are_never_shown() { - let values = - layer(json!({ "htpasswd": "users", "mycelium-rest-url": "http://u:pw@h:8989" })); + let values = layer( + json!({ "htpasswd": "users", "node-key-file": "node.key", "mycelium-rest-url": "http://u:pw@h:8989" }), + ); assert_eq!( values["mycelium-rest-url"].shown("mycelium-rest-url"), "" -- cgit v1.3.1