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
|
//! 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 (`--node-key-create`), never because
//! a path was mistyped.
use std::path::Path;
use anyhow::{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, created) = crate::files::key_file(
path,
"node key",
create,
"node-key-create",
"a new key is a new node",
KEY_BYTES,
)?;
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()
);
};
let key = Self::from_seed(seed);
if created {
info!(path = %path.display(), node_id = %key.id(), "Created a new node key: this is a new node");
}
Ok(key)
}
/// 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),
}
}
/// 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::*;
use crate::files::scratch_dir;
#[test]
fn created_only_when_asked_and_then_kept() {
let path = scratch_dir("identity-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_dir("identity-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]));
}
}
|