summaryrefslogtreecommitdiffstats
path: root/src/identity.rs
blob: 8f7fdda573dd273dc0b8b453bae66069b661ddfc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
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]));
    }
}