//! Who a node is: its node key (`--node-key-file`), an Ed25519 key of its own, and the //! node ID derived from it, `continuum:`. //! //! 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 //! a path was mistyped. use std::io::Write; use std::path::Path; use anyhow::{Context, Result, bail}; use ed25519_dalek::SigningKey; use tracing::info; use crate::transport::Hex; /// What every node ID starts with. pub(crate) const NODE_ID_PREFIX: &str = "continuum:"; /// Bytes in a key file: the Ed25519 secret key (its seed), raw. const KEY_BYTES: usize = 32; /// This node's key. pub(crate) struct NodeKey { signing: SigningKey, } 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 { 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::swarm::auth::warn_if_readable_by_others(path, "node key"); 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}", path.display(), bytes.len() ); }; Ok(Self::from_seed(seed)) } fn from_seed(seed: [u8; KEY_BYTES]) -> Self { Self { signing: SigningKey::from_bytes(&seed), } } /// A new key in a new file at `path` (only its owner may read it). fn create(path: &Path) -> Result { 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 { format!( "{NODE_ID_PREFIX}{}", Hex(self.signing.verifying_key().as_bytes()) ) } } /// Whether `id` has the form of a node ID. pub(crate) fn is_node_id(id: &str) -> bool { id.strip_prefix(NODE_ID_PREFIX).is_some_and(|hex| { hex.len() == 2 * KEY_BYTES && hex .bytes() .all(|byte| matches!(byte, b'0'..=b'9' | b'a'..=b'f')) }) } #[cfg(test)] mod tests { use super::*; fn scratch(name: &str) -> std::path::PathBuf { let dir = std::env::temp_dir().join(format!("continuum-identity-{name}-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); dir } #[test] fn created_only_when_asked_and_then_kept() { let path = scratch("create").join("node.key"); assert!(NodeKey::load(&path, false).is_err()); assert!(!path.exists()); let created = NodeKey::load(&path, true).unwrap(); assert!(is_node_id(&created.id())); assert_eq!(std::fs::read(&path).unwrap().len(), KEY_BYTES); // The same node from now on, with or without the flag. assert_eq!(NodeKey::load(&path, false).unwrap().id(), created.id()); assert_eq!(NodeKey::load(&path, true).unwrap().id(), created.id()); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let mode = std::fs::metadata(&path).unwrap().permissions().mode(); assert_eq!(mode & 0o777, 0o600); } } #[test] fn a_key_of_the_wrong_size_is_refused() { let path = scratch("size").join("node.key"); std::fs::write(&path, [7u8; 16]).unwrap(); assert!(NodeKey::load(&path, true).is_err()); } #[test] fn node_ids() { let id = NodeKey::from_seed([1; KEY_BYTES]).id(); assert!(is_node_id(&id)); assert_eq!(id.len(), NODE_ID_PREFIX.len() + 64); assert!(!is_node_id(&id[NODE_ID_PREFIX.len()..])); // a Mycelium key is no node ID assert!(!is_node_id(&id.to_uppercase())); assert!(!is_node_id(&id[..id.len() - 1])); } }