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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
|
//! 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))
}
/// A key from its 32 secret bytes: for simulated nodes, which have no key files.
pub(crate) 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 {
node_id_of(&self.public())
}
/// The public key, as others check signatures with it.
pub(crate) fn public(&self) -> [u8; 32] {
self.signing.verifying_key().to_bytes()
}
pub(crate) fn sign(&self, message: &[u8]) -> [u8; 64] {
use ed25519_dalek::Signer;
self.signing.sign(message).to_bytes()
}
}
/// The node ID of the node with public key `key`.
pub(crate) fn node_id_of(key: &[u8; 32]) -> String {
format!("{NODE_ID_PREFIX}{}", Hex(key))
}
/// Whether `signature` is `key`'s over `message`.
pub(crate) fn verify(key: &[u8; 32], message: &[u8], signature: &[u8; 64]) -> bool {
let Ok(key) = ed25519_dalek::VerifyingKey::from_bytes(key) else {
return false;
};
key.verify_strict(message, &ed25519_dalek::Signature::from_bytes(signature))
.is_ok()
}
/// 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 signatures() {
let key = NodeKey::from_seed([2; KEY_BYTES]);
let signature = key.sign(b"message");
assert!(verify(&key.public(), b"message", &signature));
assert!(!verify(&key.public(), b"massage", &signature));
let other = NodeKey::from_seed([3; KEY_BYTES]);
assert!(!verify(&other.public(), b"message", &signature));
assert_eq!(node_id_of(&key.public()), key.id());
}
#[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]));
}
}
|