Skip to main content

nym_node_requests/api/
mod.rs

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