-
Notifications
You must be signed in to change notification settings - Fork 19
Expand file tree
/
Copy pathpeer.rs
More file actions
346 lines (320 loc) · 11.4 KB
/
peer.rs
File metadata and controls
346 lines (320 loc) · 11.4 KB
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
//! Peer structures for managing known peers the network.
//! [`PeerEntry`] is be signed by the peer such that [`PeerEntry`] structs can be gossipped around
//! the network safely.
use anyhow::{anyhow, bail};
use multiaddr::{Multiaddr, PeerId};
use serde::{Deserialize, Serialize};
use ssi::jws::DecodedJWS;
use crate::{node_id::NodeKey, signer::Signer, DeserializeExt as _, NodeId, SerializeExt as _};
const MIN_EXPIRATION: u64 = 0;
// 11 9s is the maximum value we can encode into the string representation of a PeerKey.
const MAX_EXPIRATION: u64 = 99_999_999_999;
/// Peer entry that is signed and can be shared.
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct PeerEntry {
id: NodeId,
// Number of seconds after UNIX epoch when this entry is no longer valid.
expiration: u64,
addresses: Vec<Multiaddr>,
}
impl PeerEntry {
/// Construct an entry about a peer with address that is no longer valid after expiration seconds after the
/// UNIX epoch.
pub fn new(local_id: NodeId, expiration: u64, addresses: Vec<Multiaddr>) -> Self {
let peer_id = local_id.peer_id();
Self {
id: local_id,
expiration,
addresses: addresses
.into_iter()
.map(|addr| ensure_multiaddr_has_p2p(addr, peer_id))
.collect(),
}
}
fn from_jws(jws: &str) -> anyhow::Result<Self> {
let (header_b64, payload_enc, signature_b64) = ssi::jws::split_jws(jws)?;
let DecodedJWS {
header,
signing_input,
payload,
signature,
} = ssi::jws::decode_jws_parts(header_b64, payload_enc.as_bytes(), signature_b64)?;
let mut entry = PeerEntry::from_json(&payload)?;
let peer_id = entry.id.peer_id();
entry.addresses = entry
.addresses
.into_iter()
.map(|addr| ensure_multiaddr_has_p2p(addr, peer_id))
.collect();
let key = entry.id.jwk();
ssi::jws::verify_bytes(header.algorithm, &signing_input, &key, &signature)?;
Ok(entry)
}
fn to_jws(&self, signer: impl Signer) -> anyhow::Result<String> {
let entry = self.to_json()?;
signer.sign_jws(&entry)
}
/// Report the id of this peer.
pub fn id(&self) -> NodeId {
self.id
}
/// Report the number of seconds after the UNIX epoch when this entry is no longer valid.
pub fn expiration(&self) -> u64 {
self.expiration
}
/// Report the addresses where this peer can be dialed. These are guaranteed to contain the
/// peer id within the address.
pub fn addresses(&self) -> &[Multiaddr] {
&self.addresses
}
}
/// Returns a the provided multiaddr ensuring it contains the specified peer id.
pub fn ensure_multiaddr_has_p2p(addr: Multiaddr, peer_id: PeerId) -> Multiaddr {
if !addr.iter().any(|protocol| match protocol {
multiaddr::Protocol::P2p(id) => id == peer_id,
_ => false,
}) {
addr.with(multiaddr::Protocol::P2p(peer_id))
} else {
addr
}
}
/// Encoded [`PeerEntry`] prefixed with its expiration.
/// The sort order matters as its used in a Recon ring.
/// The key is valid utf-8 of the form `<expiration>.<jws>`;
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Serialize, Deserialize)]
pub struct PeerKey(String);
impl std::fmt::Display for PeerKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl TryFrom<Vec<u8>> for PeerKey {
type Error = anyhow::Error;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
let key = Self(String::from_utf8(value)?);
let _ = key.to_entry()?;
Ok(key)
}
}
impl PeerKey {
/// Return a builder for constructing a PeerKey from its parts.
pub fn builder() -> Builder<Init> {
Builder { state: Init }
}
/// Return the raw bytes of the peer key.
pub fn as_slice(&self) -> &[u8] {
self.0.as_bytes()
}
/// Report if this key contains an jws section.
pub fn has_jws(&self) -> bool {
self.0.contains('.')
}
/// Construct a signed key from a [`PeerEntry`].
pub fn from_entry(entry: &PeerEntry, node_key: &NodeKey) -> anyhow::Result<Self> {
if entry.id() != node_key.id() {
bail!("peer key must be signed by its own ID")
}
Ok(Self(format!(
// 11 digits of a timestamp gets us 1000+ years of padding for a consistent sort order.
"{:0>11}.{}",
entry.expiration,
entry.to_jws(node_key)?
)))
}
/// Decode and verify key as a [`PeerEntry`].
pub fn to_entry(&self) -> anyhow::Result<PeerEntry> {
let (expiration, jws) = self.split_expiration()?;
let peer = PeerEntry::from_jws(jws)?;
if expiration != peer.expiration {
Err(anyhow!(
"peer key expiration must match peer entry: {expiration} != {}",
peer.expiration
))
} else {
Ok(peer)
}
}
fn split_expiration(&self) -> anyhow::Result<(u64, &str)> {
let (expiration, jws) = self
.0
.split_once('.')
.ok_or_else(|| anyhow!("peer key must contain a '.'"))?;
let expiration = expiration.parse()?;
Ok((expiration, jws))
}
}
/// Builder provides an ordered API for constructing a PeerKey
#[derive(Debug)]
pub struct Builder<S: BuilderState> {
state: S,
}
/// The state of the builder
pub trait BuilderState {}
/// Initial state of the builder.
#[derive(Debug)]
pub struct Init;
impl BuilderState for Init {}
/// Build state where the expiration is known.
pub struct WithExpiration {
expiration: u64,
}
impl BuilderState for WithExpiration {}
/// Build state where the peer id is known.
pub struct WithId<'a> {
node_key: &'a NodeKey,
expiration: u64,
}
impl BuilderState for WithId<'_> {}
/// Build state where the addresses are known.
pub struct WithAddresses<'a> {
node_key: &'a NodeKey,
expiration: u64,
addresses: Vec<Multiaddr>,
}
impl BuilderState for WithAddresses<'_> {}
impl Builder<Init> {
/// Set the expiration to earliest possible value.
pub fn with_min_expiration(self) -> Builder<WithExpiration> {
Builder {
state: WithExpiration {
expiration: MIN_EXPIRATION,
},
}
}
/// Set the expiration to the latest possible value.
pub fn with_max_expiration(self) -> Builder<WithExpiration> {
Builder {
state: WithExpiration {
expiration: MAX_EXPIRATION,
},
}
}
/// Set the expiration as the number of seconds since the UNIX epoch.
pub fn with_expiration(self, expiration: u64) -> Builder<WithExpiration> {
Builder {
state: WithExpiration { expiration },
}
}
}
impl Builder<WithExpiration> {
/// Finish the build producing a partial [`PeerKey`].
pub fn build_fencepost(self) -> PeerKey {
PeerKey(format!("{:0>11}", self.state.expiration))
}
/// Set the peer id. Note, a NodeKey is required so the [`PeerEntry`] can be signed.
pub fn with_id(self, id: &NodeKey) -> Builder<WithId<'_>> {
Builder {
state: WithId {
node_key: id,
expiration: self.state.expiration,
},
}
}
}
impl<'a> Builder<WithId<'a>> {
/// Set the addresses where the peer can be reached.
pub fn with_addresses(self, addresses: Vec<Multiaddr>) -> Builder<WithAddresses<'a>> {
Builder {
state: WithAddresses {
node_key: self.state.node_key,
expiration: self.state.expiration,
addresses,
},
}
}
}
impl Builder<WithAddresses<'_>> {
/// Finish the build producing a [`PeerKey`].
pub fn build(self) -> PeerKey {
let entry = PeerEntry::new(
self.state.node_key.id(),
self.state.expiration,
self.state.addresses,
);
PeerKey::from_entry(&entry, self.state.node_key)
.expect("builder should not build invalid peer key")
}
}
#[cfg(test)]
mod tests {
use super::{PeerEntry, PeerKey};
use anyhow::Result;
use expect_test::expect;
use test_log::test;
use tracing::debug;
use crate::node_id::NodeKey;
#[test]
fn peer_roundtrip() -> Result<()> {
let node_key = NodeKey::random();
let entry = PeerEntry::new(
node_key.id(),
1732211100,
["/ip4/127.0.0.1/tcp/5100", "/ip4/127.0.0.2/udp/5100/quic-v1"]
.into_iter()
.map(std::str::FromStr::from_str)
.collect::<Result<_, _>>()?,
);
debug!(?entry, "peer entry");
let key = PeerKey::from_entry(&entry, &node_key)?;
debug!(?key, "peer key");
assert_eq!(entry, key.to_entry()?);
Ok(())
}
#[test]
fn peer_entry_p2p_multiaddrs() -> Result<()> {
let node_key =
NodeKey::try_from_secret("z3u2WLX8jeyN6sfbDowLGudoZHudxgVkNJfrw2TDTVx4tijd")?;
let entry = PeerEntry::new(
node_key.id(),
1732211100,
["/ip4/127.0.0.1/tcp/5100", "/ip4/127.0.0.2/udp/5100/quic-v1"]
.into_iter()
.map(std::str::FromStr::from_str)
.collect::<Result<_, _>>()?,
);
expect![[r#"
PeerEntry {
id: did:key:z6MkueF19qChpGQJBJXcXjfoM1MYCwC167RMwUiNWXXvEm1M,
expiration: 1732211100,
addresses: [
/ip4/127.0.0.1/tcp/5100/p2p/12D3KooWR1M8JiXyfdBKUhCLUmTJGhtNsgxnhvFVD4AU4EioDUwu,
/ip4/127.0.0.2/udp/5100/quic-v1/p2p/12D3KooWR1M8JiXyfdBKUhCLUmTJGhtNsgxnhvFVD4AU4EioDUwu,
],
}
"#]]
.assert_debug_eq(&entry);
let key = PeerKey::from_entry(&entry, &node_key)?;
expect![[r#"
PeerKey(
"01732211100.eyJhbGciOiJFZERTQSIsImtpZCI6Ino2TWt1ZUYxOXFDaHBHUUpCSlhjWGpmb00xTVlDd0MxNjdSTXdVaU5XWFh2RW0xTSJ9.eyJpZCI6eyJwdWJsaWNfZWQyNTUxOV9rZXlfYnl0ZXMiOlsyMjUsMTc1LDEzMSwxODYsNDIsNTIsMTg2LDEyMiw0OCwxMzEsOTIsNTIsMTI3LDE4MywyMjYsMTcsMiw2MCwxMDgsMTY2LDEwMCw0NCwyMTksMzIsMTgsMjMwLDI0Miw2NywxNTQsMTg0LDE1NCw5Ml19LCJleHBpcmF0aW9uIjoxNzMyMjExMTAwLCJhZGRyZXNzZXMiOlsiL2lwNC8xMjcuMC4wLjEvdGNwLzUxMDAvcDJwLzEyRDNLb29XUjFNOEppWHlmZEJLVWhDTFVtVEpHaHROc2d4bmh2RlZENEFVNEVpb0RVd3UiLCIvaXA0LzEyNy4wLjAuMi91ZHAvNTEwMC9xdWljLXYxL3AycC8xMkQzS29vV1IxTThKaVh5ZmRCS1VoQ0xVbVRKR2h0TnNneG5odkZWRDRBVTRFaW9EVXd1Il19.X1LOJlSQSMAMyYhO8OhpjKJ-Q2SqoTuw6Ak-O6ZZN6oEl1XNLsuf2smq5CotYZPTKhRqPazwBEZzm5K3SEz1Cw",
)
"#]].assert_debug_eq(&key);
Ok(())
}
#[test]
fn peer_jws_verify() -> Result<()> {
let n1 = NodeKey::random();
let n2 = NodeKey::random();
let entry = PeerEntry::new(
n1.id(),
1732211100,
["/ip4/127.0.0.1/tcp/5100", "/ip4/127.0.0.2/udp/5100/quic-v1"]
.into_iter()
.map(std::str::FromStr::from_str)
.collect::<Result<_, _>>()?,
);
let jws = entry.to_jws(&n2)?;
expect![[r#"
Err(
JWK(
CryptoErr(
signature::Error { source: Some(Verification equation was not satisfied) },
),
),
)
"#]]
.assert_debug_eq(&PeerEntry::from_jws(&jws));
Ok(())
}
}