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