diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/config/mod.rs | 45 | ||||
| -rw-r--r-- | src/files.rs | 51 | ||||
| -rw-r--r-- | src/identity.rs | 58 | ||||
| -rw-r--r-- | src/main.rs | 7 | ||||
| -rw-r--r-- | src/swarm/auth.rs | 30 | ||||
| -rw-r--r-- | src/transport/libp2p/config.rs | 31 | ||||
| -rw-r--r-- | src/transport/mycelium/mod.rs | 28 |
7 files changed, 187 insertions, 63 deletions
diff --git a/src/config/mod.rs b/src/config/mod.rs index ea323e1..25b39ea 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -201,7 +201,7 @@ pub(crate) const OPTIONS: &[OptionSpec] = &[ "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, but with relay-only (then not allowed).", ), option( - "create-node-key", + "node-key-create", Kind::Flag, "BOOL", Some("false"), @@ -215,6 +215,13 @@ pub(crate) const OPTIONS: &[OptionSpec] = &[ "Pre-shared swarm key: only nodes that know it may join (raw bytes, at least 16; e.g. `head -c 32 /dev/urandom`). Without it, any node that can reach a member may join.", ), option( + "swarm-key-create", + Kind::Flag, + "BOOL", + Some("false"), + "Create a new swarm key at swarm-key-file if there is no file there: on the swarm's first node, as the others need a copy of it (a key of their own keeps them out). Without it, a missing key file is an error.", + ), + option( "alias", Kind::Text, "NAME", @@ -373,6 +380,13 @@ pub(crate) const OPTIONS: &[OptionSpec] = &[ "With the libp2p transport: its key (Ed25519, 32 raw bytes), which makes its peer ID; a new one each start if absent (the node ID stays the same either way).", ), option( + "libp2p-key-create", + Kind::Flag, + "BOOL", + Some("false"), + "With the libp2p transport: create a new libp2p key at libp2p-key-file if there is no file there (a new peer ID). Without it, a missing key file is an error.", + ), + option( "mycelium-rest-url", Kind::Text, "SCHEME://HOSTNAME:PORT", @@ -389,6 +403,13 @@ pub(crate) const OPTIONS: &[OptionSpec] = &[ "Private key of the supervised Mycelium daemon (its identity); a new one each start if absent.", ), option( + "mycelium-key-create", + Kind::Flag, + "BOOL", + Some("false"), + "Create a new key for the supervised Mycelium daemon at mycelium-key-file if there is no file there (a new Mycelium address). Without it, a missing key file is an error.", + ), + option( "mycelium-network-key-file", Kind::Text, "PATH", @@ -470,8 +491,10 @@ pub(crate) struct Settings { /// Required (`check`), but not allowed with `relay_only`. pub(crate) node_key_file: Option<PathBuf>, #[serde(default)] - pub(crate) create_node_key: bool, + pub(crate) node_key_create: bool, pub(crate) swarm_key_file: Option<PathBuf>, + #[serde(default)] + pub(crate) swarm_key_create: bool, pub(crate) alias: Option<String>, #[serde(default = "default_keepalive", with = "crate::units::duration_option")] pub(crate) keepalive: Duration, @@ -530,6 +553,9 @@ pub(crate) struct Settings { pub(crate) libp2p_public: bool, #[cfg_attr(not(feature = "libp2p"), allow(dead_code))] pub(crate) libp2p_key_file: Option<PathBuf>, + #[serde(default)] + #[cfg_attr(not(feature = "libp2p"), allow(dead_code))] + pub(crate) libp2p_key_create: bool, #[cfg_attr(not(feature = "tor"), allow(dead_code))] pub(crate) tor_state_dir: Option<PathBuf>, #[serde(default = "default_tor_port")] @@ -541,6 +567,9 @@ pub(crate) struct Settings { pub(crate) mycelium_rest_url: Option<String>, #[cfg_attr(not(feature = "mycelium"), allow(dead_code))] pub(crate) mycelium_key_file: Option<PathBuf>, + #[serde(default)] + #[cfg_attr(not(feature = "mycelium"), allow(dead_code))] + pub(crate) mycelium_key_create: bool, #[cfg_attr(not(feature = "mycelium"), allow(dead_code))] pub(crate) mycelium_network_key_file: Option<PathBuf>, #[cfg_attr(not(feature = "mycelium"), allow(dead_code))] @@ -624,6 +653,12 @@ impl Settings { if self.genesis && self.recover_from.is_some() { bail!("recover-from starts the swarm from a backup; it can't be combined with genesis"); } + if self.node_key_create && self.node_key_file.is_none() { + bail!("node-key-create creates the key at node-key-file; set that too"); + } + if self.swarm_key_create && self.swarm_key_file.is_none() { + bail!("swarm-key-create creates the key at swarm-key-file; set that too"); + } if self.relay_only { self.check_relay_only()?; } else { @@ -632,7 +667,7 @@ impl Settings { } if self.node_key_file.is_none() { bail!( - "node-key-file is required: who this node is (create one with create-node-key)" + "node-key-file is required: who this node is (create one with node-key-create)" ); } } @@ -706,7 +741,7 @@ impl Settings { // Its listen address and key: libp2p's checks (`transport::libp2p::check`). let swarm_options = [ ("node-key-file", self.node_key_file.is_some()), - ("create-node-key", self.create_node_key), + ("node-key-create", self.node_key_create), ("seeds", !self.seeds.is_empty()), ("genesis", self.genesis), ("state-file", self.state_file.is_some()), @@ -714,6 +749,7 @@ impl Settings { ("recover-from", self.recover_from.is_some()), ("remake", self.remake.is_some()), ("swarm-key-file", self.swarm_key_file.is_some()), + ("swarm-key-create", self.swarm_key_create), ("protocol-writers", !self.protocol_writers.is_empty()), ("alias", self.alias.is_some()), ("debug-api", self.debug_api), @@ -840,6 +876,7 @@ const CONFLICTS: &[(&[&str], &[&str])] = &[( &["mycelium-rest-url"], &[ "mycelium-key-file", + "mycelium-key-create", "mycelium-network-key-file", "mycelium-network-name", "mycelium-tcp-listen-port", diff --git a/src/files.rs b/src/files.rs index cd2f38c..bc8fb8e 100644 --- a/src/files.rs +++ b/src/files.rs @@ -167,6 +167,57 @@ fn sync_parent(path: &Path) { } } +/// A key file's bytes: read from `path`; or with `create`, if there is no file there, +/// `len` random bytes in a new one only its owner may read (then also `true`). Without +/// `create`, a missing file is an error saying to give `create_option` (`what`: which +/// key; `hint`: what a new one means). Never created where a file is: a mistyped path +/// stays an error unless asked. +pub(crate) fn key_file( + path: &Path, + what: &str, + create: bool, + create_option: &str, + hint: &str, + len: usize, +) -> anyhow::Result<(Vec<u8>, bool)> { + use anyhow::Context; + use std::io::Write; + match std::fs::read(path) { + Ok(bytes) => { + warn_if_readable_by_others(path, what); + Ok((bytes, false)) + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + if !create { + anyhow::bail!( + "The {what} file {} doesn't exist; add --{create_option} to create a new {what} there ({hint})", + path.display() + ); + } + let mut bytes = vec![0u8; len]; + getrandom::fill(&mut bytes) + .map_err(|error| anyhow::anyhow!("No randomness: {error}"))?; + let mut options = std::fs::OpenOptions::new(); + options.write(true).create_new(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + let written = (|| { + let mut file = options.open(path)?; + file.write_all(&bytes)?; + file.sync_all() + })(); + written.with_context(|| format!("Creating the {what} file {}", path.display()))?; + Ok((bytes, true)) + } + Err(error) => { + Err(error).with_context(|| format!("Reading the {what} from {}", path.display())) + } + } +} + /// A key is a secret: a file others may read is worth a warning (not an error: a /// group may be deliberate, e.g. shared with a deployment tool). `what`: which key. pub(crate) fn warn_if_readable_by_others(path: &Path, what: &str) { diff --git a/src/identity.rs b/src/identity.rs index 8efe30f..04b7e6f 100644 --- a/src/identity.rs +++ b/src/identity.rs @@ -3,13 +3,12 @@ //! //! The node ID names the node whatever transport reaches it: a transport address only //! says where it is (`docs/peers.md`, node IDs and addresses). A new key is a new -//! node, so the key is created only when asked to (`--create-node-key`), never because +//! node, so the key is created only when asked to (`--node-key-create`), never because //! a path was mistyped. -use std::io::Write; use std::path::Path; -use anyhow::{Context, Result, bail}; +use anyhow::{Result, bail}; use ed25519_dalek::SigningKey; use tracing::info; @@ -30,25 +29,14 @@ impl NodeKey { /// Reads the key from `path`; with `create`, makes a new one there if the file /// doesn't exist. pub(crate) fn load(path: &Path, create: bool) -> Result<Self> { - let bytes = match std::fs::read(path) { - Ok(bytes) => bytes, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => { - if !create { - bail!( - "The node key file {} doesn't exist; add --create-node-key to create a new node key there (a new key is a new node)", - path.display() - ); - } - let key = Self::create(path)?; - info!(path = %path.display(), node_id = %key.id(), "Created a new node key: this is a new node"); - return Ok(key); - } - Err(error) => { - return Err(error) - .with_context(|| format!("Reading the node key from {}", path.display())); - } - }; - crate::files::warn_if_readable_by_others(path, "node key"); + let (bytes, created) = crate::files::key_file( + path, + "node key", + create, + "node-key-create", + "a new key is a new node", + KEY_BYTES, + )?; let Ok(seed) = <[u8; KEY_BYTES]>::try_from(bytes.as_slice()) else { bail!( "The node key in {} has {} bytes; a node key has {KEY_BYTES}", @@ -56,7 +44,11 @@ impl NodeKey { bytes.len() ); }; - Ok(Self::from_seed(seed)) + let key = Self::from_seed(seed); + if created { + info!(path = %path.display(), node_id = %key.id(), "Created a new node key: this is a new node"); + } + Ok(key) } /// A key from its 32 secret bytes: for simulated nodes, which have no key files. @@ -66,26 +58,6 @@ impl NodeKey { } } - /// A new key in a new file at `path` (only its owner may read it). - fn create(path: &Path) -> Result<Self> { - let mut seed = [0u8; KEY_BYTES]; - getrandom::fill(&mut seed).map_err(|error| anyhow::anyhow!("No randomness: {error}"))?; - let mut options = std::fs::OpenOptions::new(); - options.write(true).create_new(true); - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - options.mode(0o600); - } - let written = (|| { - let mut file = options.open(path)?; - file.write_all(&seed)?; - file.sync_all() - })(); - written.with_context(|| format!("Creating the node key file {}", path.display()))?; - Ok(Self::from_seed(seed)) - } - /// This node's ID. pub(crate) fn id(&self) -> String { node_id_of(&self.public()) diff --git a/src/main.rs b/src/main.rs index a725174..a6f2937 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,11 +76,14 @@ async fn main() -> Result<()> { .context("node-key-file is required")?; let node_key = Arc::new(identity::NodeKey::load( node_key_file, - args.create_node_key, + args.node_key_create, )?); info!(node_id = %node_key.id(), "Node identity"); let swarm_key = match &args.swarm_key_file { - Some(path) => Some(Arc::new(swarm::auth::SwarmKey::from_file(path)?)), + Some(path) => Some(Arc::new(swarm::auth::SwarmKey::from_file( + path, + args.swarm_key_create, + )?)), None => { warn!("No swarm key: any node that can reach a member may join"); None diff --git a/src/swarm/auth.rs b/src/swarm/auth.rs index 534a64d..98a004c 100644 --- a/src/swarm/auth.rs +++ b/src/swarm/auth.rs @@ -21,7 +21,7 @@ use std::sync::Mutex; use std::time::{Duration, Instant}; use crate::locks::MutexExt; -use anyhow::{Context, Result, bail}; +use anyhow::{Result, bail}; use sha2::{Digest, Sha256}; const HELLO_LABEL: &[u8] = b"continuum/hello/1"; @@ -46,10 +46,19 @@ const REJECTION_LOG_INTERVAL: Duration = Duration::from_secs(10); impl SwarmKey { /// Reads the key from a file; its bytes are the key, as they are. Generate one with /// e.g. `head -c 32 /dev/urandom > swarm.key`. - pub(crate) fn from_file(path: &Path) -> Result<Self> { - let key = std::fs::read(path) - .with_context(|| format!("Reading the swarm key from {}", path.display()))?; - crate::files::warn_if_readable_by_others(path, "swarm key"); + /// With `create`, a new one (32 random bytes) if there is no file at `path`. + pub(crate) fn from_file(path: &Path, create: bool) -> Result<Self> { + let (key, created) = crate::files::key_file( + path, + "swarm key", + create, + "swarm-key-create", + "on the swarm's first node; the others need a copy of it", + 32, + )?; + if created { + tracing::info!(path = %path.display(), "Created a new swarm key: copy it to the swarm's other nodes"); + } if key.len() < 16 { bail!( "The swarm key in {} has {} bytes; use at least 16 (32 random bytes recommended)", @@ -154,6 +163,17 @@ mod tests { crate::transport::Hex(bytes).to_string() } + /// A swarm key file is made only when asked, then read as any other. + #[test] + fn swarm_key_created_only_when_asked() { + let path = crate::files::scratch_dir("swarm-key-create").join("swarm.key"); + assert!(SwarmKey::from_file(&path, false).is_err()); + assert!(!path.exists()); + let created = SwarmKey::from_file(&path, true).unwrap(); + assert_eq!(created.key.len(), 32); + assert_eq!(SwarmKey::from_file(&path, false).unwrap().key, created.key); + } + #[test] fn hmac_matches_rfc_4231() { // Test case 2. diff --git a/src/transport/libp2p/config.rs b/src/transport/libp2p/config.rs index 6d26224..348a170 100644 --- a/src/transport/libp2p/config.rs +++ b/src/transport/libp2p/config.rs @@ -70,6 +70,9 @@ pub(crate) fn check(settings: &crate::config::Settings) -> Result<()> { &settings.libp2p_ignore, &settings.libp2p_prefer, )?; + if settings.libp2p_key_create && settings.libp2p_key_file.is_none() { + bail!("libp2p-key-create creates the key at libp2p-key-file; set that too"); + } if settings.relay_only { if settings.libp2p_listen.is_empty() { bail!("relay-only needs libp2p-listen: where nodes reach the relay"); @@ -119,7 +122,10 @@ impl Libp2pConfig { ) -> Result<Self> { let parse = parse_addresses; Ok(Self { - key: Self::load_key(settings.libp2p_key_file.as_deref())?, + key: Self::load_key( + settings.libp2p_key_file.as_deref(), + settings.libp2p_key_create, + )?, listen: parse("libp2p-listen", &settings.libp2p_listen)?, announce: parse("libp2p-announce", &settings.libp2p_announce)?, relays: parse("libp2p-relays", &settings.libp2p_relays)?, @@ -135,16 +141,25 @@ impl Libp2pConfig { }) } - /// The key in `path` (32 raw bytes, an Ed25519 secret key), or a new one each start - /// without a file: the peer ID only says where a node is, so a new one costs nothing - /// but the WHO that finds it again (`transport::identity`). - pub(crate) fn load_key(path: Option<&Path>) -> Result<Keypair> { + /// The key in `path` (32 raw bytes, an Ed25519 secret key), created there with + /// `create` if there is no file; or a new one each start without a path: the peer + /// ID only says where a node is, so a new one costs nothing but the WHO that finds + /// it again (`transport::identity`). + pub(crate) fn load_key(path: Option<&Path>, create: bool) -> Result<Keypair> { let Some(path) = path else { return Ok(Keypair::generate_ed25519()); }; - let mut bytes = std::fs::read(path) - .with_context(|| format!("Reading the libp2p key from {}", path.display()))?; - crate::files::warn_if_readable_by_others(path, "libp2p key"); + let (mut bytes, created) = crate::files::key_file( + path, + "libp2p key", + create, + "libp2p-key-create", + "a new peer ID", + 32, + )?; + if created { + info!(path = %path.display(), "Created a new libp2p key"); + } if bytes.len() != 32 { bail!( "The libp2p key in {} has {} bytes; it has 32", diff --git a/src/transport/mycelium/mod.rs b/src/transport/mycelium/mod.rs index 21c939d..9078134 100644 --- a/src/transport/mycelium/mod.rs +++ b/src/transport/mycelium/mod.rs @@ -104,6 +104,9 @@ pub(crate) fn check(settings: &crate::config::Settings) -> Result<()> { ); } } + if settings.mycelium_key_create && settings.mycelium_key_file.is_none() { + anyhow::bail!("mycelium-key-create creates the key at mycelium-key-file; set that too"); + } // A private network has both; one alone would be ignored or invented. if settings.mycelium_network_key_file.is_some() != settings.mycelium_network_name.is_some() { anyhow::bail!("mycelium-network-key-file and mycelium-network-name go together"); @@ -113,9 +116,10 @@ pub(crate) fn check(settings: &crate::config::Settings) -> Result<()> { /// The options only a supervised Mycelium daemon takes, and whether each is set: the /// other side of `config::CONFLICTS`' `mycelium-rest-url` (a test keeps them the same). -pub(crate) fn supervised_only(settings: &crate::config::Settings) -> [(&'static str, bool); 4] { +pub(crate) fn supervised_only(settings: &crate::config::Settings) -> [(&'static str, bool); 5] { [ ("mycelium-key-file", settings.mycelium_key_file.is_some()), + ("mycelium-key-create", settings.mycelium_key_create), ( "mycelium-network-key-file", settings.mycelium_network_key_file.is_some(), @@ -138,6 +142,28 @@ pub(crate) async fn start( settings: &crate::config::Settings, mut stop: tokio::sync::watch::Receiver<bool>, ) -> Result<Option<MyceliumTransport>> { + // The daemon's key, 32 raw bytes (as `daemon` hands it an ephemeral one): read, + // or made, here, so a missing file fails before the daemon starts. + if let Some(path) = &settings.mycelium_key_file { + let (bytes, created) = crate::files::key_file( + path, + "Mycelium key", + settings.mycelium_key_create, + "mycelium-key-create", + "a new Mycelium address", + 32, + )?; + if bytes.len() != 32 { + anyhow::bail!( + "The Mycelium key in {} has {} bytes; it has 32", + path.display(), + bytes.len() + ); + } + if created { + tracing::info!(path = %path.display(), "Created a new Mycelium key"); + } + } let spawn_config = MyceliumSpawnConfig { rest_url_opt: settings .mycelium_rest_url |
