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