summaryrefslogtreecommitdiffstats
path: root/src/identity.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/identity.rs')
-rw-r--r--src/identity.rs155
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]));
+ }
+}