1use crate::api::v1::node::models::{
5 LegacyHostInformationV1, LegacyHostInformationV2, LegacyHostInformationV3,
6};
7use crate::error::Error;
8use nym_crypto::asymmetric::ed25519;
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use std::fmt::{Display, Formatter};
12use std::ops::Deref;
13
14#[cfg(feature = "client")]
15pub mod client;
16pub mod v1;
17
18#[cfg(feature = "client")]
19pub use client::Client;
20
21pub type SignedHostInformation = SignedData<crate::api::v1::node::models::HostInformation>;
23
24#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
25pub struct SignedDataHostInfo {
26 pub data: crate::api::v1::node::models::HostInformation,
28 pub signature: String,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct SignedData<T> {
33 pub data: T,
35 pub signature: String,
36}
37
38impl<T> SignedData<T> {
39 pub fn new(data: T, key: &ed25519::PrivateKey) -> Result<Self, Error>
40 where
41 T: Serialize,
42 {
43 let plaintext = serde_json::to_string(&data)?;
44
45 let signature = key.sign(plaintext).to_base58_string();
46 Ok(SignedData { data, signature })
47 }
48
49 pub fn verify(&self, key: &ed25519::PublicKey) -> bool
50 where
51 T: Serialize,
52 {
53 let Ok(plaintext) = serde_json::to_string(&self.data) else {
54 return false;
55 };
56
57 let Ok(signature) = ed25519::Signature::from_base58_string(&self.signature) else {
58 return false;
59 };
60
61 key.verify(plaintext, &signature).is_ok()
62 }
63}
64
65impl SignedHostInformation {
66 pub fn verify_host_information(&self) -> bool {
67 if self.verify(&self.keys.ed25519_identity) {
68 return true;
69 }
70
71 let legacy_v3 = SignedData {
74 data: LegacyHostInformationV3::from(self.data.clone()),
75 signature: self.signature.clone(),
76 };
77
78 if legacy_v3.verify(&self.keys.ed25519_identity) {
79 return true;
80 }
81
82 let legacy_v3 = SignedData {
84 data: LegacyHostInformationV3::from(self.data.clone()),
85 signature: self.signature.clone(),
86 };
87
88 if legacy_v3.verify(&self.keys.ed25519_identity) {
89 return true;
90 }
91
92 let legacy_v2 = SignedData {
93 data: LegacyHostInformationV2::from(legacy_v3.data),
94 signature: self.signature.clone(),
95 };
96
97 if legacy_v2.verify(&self.keys.ed25519_identity) {
98 return true;
99 }
100
101 SignedData {
102 data: LegacyHostInformationV1::from(legacy_v2.data),
103 signature: self.signature.clone(),
104 }
105 .verify(&self.keys.ed25519_identity)
106 }
107}
108
109impl<T> Deref for SignedData<T> {
110 type Target = T;
111
112 fn deref(&self) -> &Self::Target {
113 &self.data
114 }
115}
116
117#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
118pub struct ErrorResponse {
119 pub message: String,
120}
121
122impl Display for ErrorResponse {
123 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
124 self.message.fmt(f)
125 }
126}
127
128#[allow(deprecated)]
129#[cfg(test)]
130mod tests {
131
132 use super::*;
133 use crate::api::v1::node::models::{HostKeys, SphinxKey};
134 use nym_crypto::asymmetric::{ed25519, x25519};
135 use nym_noise_keys::{NoiseVersion, VersionedNoiseKey};
136 use rand_chacha::rand_core::SeedableRng;
137
138 #[test]
139 fn dummy_signed_host_verification() {
140 let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]);
141 let ed22519 = ed25519::KeyPair::new(&mut rng);
142 let x25519_sphinx = x25519::KeyPair::new(&mut rng);
143 let x25519_sphinx2 = x25519::KeyPair::new(&mut rng);
144 let x25519_versioned_noise = VersionedNoiseKey {
145 supported_version: NoiseVersion::V1,
146 x25519_pubkey: *x25519::KeyPair::new(&mut rng).public_key(),
147 };
148
149 let current_rotation_id = 1234;
150
151 let host_info = crate::api::v1::node::models::HostInformation {
153 ip_address: vec!["1.1.1.1".parse().unwrap()],
154 hostname: Some("foomp.com".to_string()),
155 keys: crate::api::v1::node::models::HostKeys {
156 ed25519_identity: *ed22519.public_key(),
157 x25519_sphinx: *x25519_sphinx.public_key(),
158 primary_x25519_sphinx_key: SphinxKey {
159 rotation_id: current_rotation_id,
160 public_key: *x25519_sphinx.public_key(),
161 },
162 pre_announced_x25519_sphinx_key: None,
163 x25519_versioned_noise: None,
164 },
165 };
166
167 let signed_info = SignedHostInformation::new(host_info, ed22519.private_key()).unwrap();
168 assert!(signed_info.verify(ed22519.public_key()));
169 assert!(signed_info.verify_host_information());
170
171 let host_info_with_noise = crate::api::v1::node::models::HostInformation {
172 ip_address: vec!["1.1.1.1".parse().unwrap()],
173 hostname: Some("foomp.com".to_string()),
174 keys: crate::api::v1::node::models::HostKeys {
175 ed25519_identity: *ed22519.public_key(),
176 x25519_sphinx: *x25519_sphinx.public_key(),
177 primary_x25519_sphinx_key: SphinxKey {
178 rotation_id: current_rotation_id,
179 public_key: *x25519_sphinx.public_key(),
180 },
181 pre_announced_x25519_sphinx_key: None,
182 x25519_versioned_noise: Some(x25519_versioned_noise),
183 },
184 };
185
186 let signed_info =
187 SignedHostInformation::new(host_info_with_noise, ed22519.private_key()).unwrap();
188 assert!(signed_info.verify(ed22519.public_key()));
189 assert!(signed_info.verify_host_information());
190
191 let host_info = crate::api::v1::node::models::HostInformation {
193 ip_address: vec!["1.1.1.1".parse().unwrap()],
194 hostname: Some("foomp.com".to_string()),
195 keys: crate::api::v1::node::models::HostKeys {
196 ed25519_identity: *ed22519.public_key(),
197 x25519_sphinx: *x25519_sphinx.public_key(),
198 primary_x25519_sphinx_key: SphinxKey {
199 rotation_id: current_rotation_id,
200 public_key: *x25519_sphinx.public_key(),
201 },
202 pre_announced_x25519_sphinx_key: Some(SphinxKey {
203 rotation_id: current_rotation_id + 1,
204 public_key: *x25519_sphinx2.public_key(),
205 }),
206 x25519_versioned_noise: None,
207 },
208 };
209
210 let signed_info = SignedHostInformation::new(host_info, ed22519.private_key()).unwrap();
211 assert!(signed_info.verify(ed22519.public_key()));
212 assert!(signed_info.verify_host_information());
213
214 let host_info_with_noise = crate::api::v1::node::models::HostInformation {
215 ip_address: vec!["1.1.1.1".parse().unwrap()],
216 hostname: Some("foomp.com".to_string()),
217 keys: crate::api::v1::node::models::HostKeys {
218 ed25519_identity: *ed22519.public_key(),
219 x25519_sphinx: *x25519_sphinx.public_key(),
220 primary_x25519_sphinx_key: SphinxKey {
221 rotation_id: current_rotation_id,
222 public_key: *x25519_sphinx.public_key(),
223 },
224 pre_announced_x25519_sphinx_key: Some(SphinxKey {
225 rotation_id: current_rotation_id + 1,
226 public_key: *x25519_sphinx2.public_key(),
227 }),
228 x25519_versioned_noise: Some(x25519_versioned_noise),
229 },
230 };
231
232 let signed_info =
233 SignedHostInformation::new(host_info_with_noise, ed22519.private_key()).unwrap();
234 assert!(signed_info.verify(ed22519.public_key()));
235 assert!(signed_info.verify_host_information());
236 }
237
238 #[test]
239 fn dummy_legacy_v3_signed_host_verification() {
240 let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]);
241 let ed22519 = ed25519::KeyPair::new(&mut rng);
242 let x25519_sphinx = x25519::KeyPair::new(&mut rng);
243 let x25519_noise = x25519::KeyPair::new(&mut rng);
244
245 let legacy_info_no_noise = crate::api::v1::node::models::LegacyHostInformationV3 {
246 ip_address: vec!["1.1.1.1".parse().unwrap()],
247 hostname: Some("foomp.com".to_string()),
248 keys: crate::api::v1::node::models::LegacyHostKeysV3 {
249 ed25519_identity: *ed22519.public_key(),
250 x25519_sphinx: *x25519_sphinx.public_key(),
251 x25519_noise: None,
252 },
253 };
254
255 let current_struct = crate::api::v1::node::models::HostInformation {
257 ip_address: vec!["1.1.1.1".parse().unwrap()],
258 hostname: Some("foomp.com".to_string()),
259 keys: HostKeys {
260 ed25519_identity: *ed22519.public_key(),
261 x25519_sphinx: *x25519_sphinx.public_key(),
262 primary_x25519_sphinx_key: SphinxKey {
263 rotation_id: u32::MAX,
264 public_key: *x25519_sphinx.public_key(),
265 },
266 pre_announced_x25519_sphinx_key: None,
267 x25519_versioned_noise: None,
268 },
269 };
270
271 let signature = SignedData::new(legacy_info_no_noise, ed22519.private_key())
273 .unwrap()
274 .signature;
275
276 let current_struct = SignedData {
278 data: current_struct,
279 signature,
280 };
281
282 assert!(!current_struct.verify(ed22519.public_key()));
283 assert!(current_struct.verify_host_information());
284
285 let legacy_info_noise = crate::api::v1::node::models::LegacyHostInformationV3 {
287 ip_address: vec!["1.1.1.1".parse().unwrap()],
288 hostname: Some("foomp.com".to_string()),
289 keys: crate::api::v1::node::models::LegacyHostKeysV3 {
290 ed25519_identity: *ed22519.public_key(),
291 x25519_sphinx: *x25519_sphinx.public_key(),
292 x25519_noise: Some(*x25519_noise.public_key()),
293 },
294 };
295
296 let current_struct_noise = crate::api::v1::node::models::HostInformation {
298 ip_address: vec!["1.1.1.1".parse().unwrap()],
299 hostname: Some("foomp.com".to_string()),
300 keys: HostKeys {
301 ed25519_identity: *ed22519.public_key(),
302 x25519_sphinx: *x25519_sphinx.public_key(),
303 primary_x25519_sphinx_key: SphinxKey {
304 rotation_id: u32::MAX,
305 public_key: *x25519_sphinx.public_key(),
306 },
307 pre_announced_x25519_sphinx_key: None,
308 x25519_versioned_noise: Some(VersionedNoiseKey {
309 supported_version: NoiseVersion::V1,
310 x25519_pubkey: legacy_info_noise.keys.x25519_noise.unwrap(),
311 }),
312 },
313 };
314
315 let signature_noise = SignedData::new(legacy_info_noise, ed22519.private_key())
318 .unwrap()
319 .signature;
320
321 let current_struct_noise = SignedData {
324 data: current_struct_noise,
325 signature: signature_noise,
326 };
327
328 assert!(!current_struct_noise.verify(ed22519.public_key()));
329 assert!(current_struct_noise.verify_host_information())
330 }
331
332 #[test]
333 fn dummy_legacy_v2_signed_host_verification() {
334 let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]);
335 let ed22519 = ed25519::KeyPair::new(&mut rng);
336 let x25519_sphinx = x25519::KeyPair::new(&mut rng);
337 let x25519_noise = x25519::KeyPair::new(&mut rng);
338
339 let legacy_info_no_noise = crate::api::v1::node::models::LegacyHostInformationV2 {
340 ip_address: vec!["1.1.1.1".parse().unwrap()],
341 hostname: Some("foomp.com".to_string()),
342 keys: crate::api::v1::node::models::LegacyHostKeysV2 {
343 ed25519_identity: ed22519.public_key().to_base58_string(),
344 x25519_sphinx: x25519_sphinx.public_key().to_base58_string(),
345 x25519_noise: "".to_string(),
346 },
347 };
348
349 let legacy_info_noise = crate::api::v1::node::models::LegacyHostInformationV2 {
350 ip_address: vec!["1.1.1.1".parse().unwrap()],
351 hostname: Some("foomp.com".to_string()),
352 keys: crate::api::v1::node::models::LegacyHostKeysV2 {
353 ed25519_identity: ed22519.public_key().to_base58_string(),
354 x25519_sphinx: x25519_sphinx.public_key().to_base58_string(),
355 x25519_noise: x25519_noise.public_key().to_base58_string(),
356 },
357 };
358
359 let host_info_no_noise = crate::api::v1::node::models::HostInformation {
361 ip_address: legacy_info_no_noise.ip_address.clone(),
362 hostname: legacy_info_no_noise.hostname.clone(),
363 keys: crate::api::v1::node::models::HostKeys {
364 ed25519_identity: legacy_info_no_noise.keys.ed25519_identity.parse().unwrap(),
365 x25519_sphinx: *x25519_sphinx.public_key(),
366 primary_x25519_sphinx_key: SphinxKey {
367 rotation_id: u32::MAX,
368 public_key: *x25519_sphinx.public_key(),
369 },
370 pre_announced_x25519_sphinx_key: None,
371 x25519_versioned_noise: None,
372 },
373 };
374
375 let host_info_noise = crate::api::v1::node::models::HostInformation {
377 ip_address: legacy_info_noise.ip_address.clone(),
378 hostname: legacy_info_noise.hostname.clone(),
379 keys: crate::api::v1::node::models::HostKeys {
380 ed25519_identity: legacy_info_noise.keys.ed25519_identity.parse().unwrap(),
381 x25519_sphinx: *x25519_sphinx.public_key(),
382 primary_x25519_sphinx_key: SphinxKey {
383 rotation_id: u32::MAX,
384 public_key: *x25519_sphinx.public_key(),
385 },
386 pre_announced_x25519_sphinx_key: None,
387 x25519_versioned_noise: Some(VersionedNoiseKey {
388 supported_version: NoiseVersion::V1,
389 x25519_pubkey: legacy_info_noise.keys.x25519_noise.parse().unwrap(),
390 }),
391 },
392 };
393
394 let signature_no_noise = SignedData::new(legacy_info_no_noise, ed22519.private_key())
396 .unwrap()
397 .signature;
398
399 let signature_noise = SignedData::new(legacy_info_noise, ed22519.private_key())
400 .unwrap()
401 .signature;
402
403 let current_struct_no_noise = SignedData {
405 data: host_info_no_noise,
406 signature: signature_no_noise,
407 };
408
409 let current_struct_noise = SignedData {
410 data: host_info_noise,
411 signature: signature_noise,
412 };
413
414 assert!(!current_struct_no_noise.verify(ed22519.public_key()));
415 assert!(current_struct_no_noise.verify_host_information());
416
417 assert!(!current_struct_noise.verify(ed22519.public_key()));
418 assert!(current_struct_noise.verify_host_information())
419 }
420
421 #[test]
422 fn dummy_legacy_v1_signed_host_verification() {
423 let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]);
424 let ed22519 = ed25519::KeyPair::new(&mut rng);
425 let x25519_sphinx = x25519::KeyPair::new(&mut rng);
426
427 let legacy_info = crate::api::v1::node::models::LegacyHostInformationV1 {
428 ip_address: vec!["1.1.1.1".parse().unwrap()],
429 hostname: Some("foomp.com".to_string()),
430 keys: crate::api::v1::node::models::LegacyHostKeysV1 {
431 ed25519: ed22519.public_key().to_base58_string(),
432 x25519: x25519_sphinx.public_key().to_base58_string(),
433 },
434 };
435
436 let host_info = crate::api::v1::node::models::HostInformation {
438 ip_address: legacy_info.ip_address.clone(),
439 hostname: legacy_info.hostname.clone(),
440 keys: crate::api::v1::node::models::HostKeys {
441 ed25519_identity: legacy_info.keys.ed25519.parse().unwrap(),
442 x25519_sphinx: *x25519_sphinx.public_key(),
443 primary_x25519_sphinx_key: SphinxKey {
444 rotation_id: u32::MAX,
445 public_key: *x25519_sphinx.public_key(),
446 },
447 pre_announced_x25519_sphinx_key: None,
448 x25519_versioned_noise: None,
449 },
450 };
451
452 let signature = SignedData::new(legacy_info, ed22519.private_key())
454 .unwrap()
455 .signature;
456
457 let current_struct = SignedData {
459 data: host_info,
460 signature,
461 };
462
463 assert!(!current_struct.verify(ed22519.public_key()));
464 assert!(current_struct.verify_host_information())
465 }
466}