diff options
Diffstat (limited to 'src/identity.rs')
| -rw-r--r-- | src/identity.rs | 155 |
1 files changed, 155 insertions, 0 deletions
diff --git a/src/identity.rs b/src/identity.rs new file mode 100644 index 0000000..8f7fdda --- /dev/null +++ b/src/identity.rs @@ -0,0 +1,155 @@ +//! Who a node is: its node key (`--node-key-file`), an Ed25519 key of its own, and the +//! node ID derived from it, `continuum:<hex of the public key>`. +//! +//! 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<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::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<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 { + 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])); + } +} |
