Files
nym/nym-node/nym-node-requests/src/api/mod.rs
T
Jędrzej Stuczyński f6bd511599 feat: Lewes Protocol with PSQv2 (#6491)
* merging georgio/lp-psqv2-integration

* use authenicator on the responder's side

* nym-lp crate compiling

* moved the e2e test to nym-lp

* move key generation to peer

* moved principal generation

* update KKTResponder

* encapsulation key parsing

* Adding concrete types within KKT exchange

* initiator side of the full handshake

* responder side of the handshake and full e2e test

* fixed unit-tests within nym-kkt

* LpSession cleanup

* helpers for Transport

* revamp of the transport traits and initial work on client-side transport

* compiling nym-crypto

* 'working' client-entry dvpn reg

* Fix key conversion

* Slightly reduce use of rand08

* reverted back to libcrux repo refs

* intial telescoping reg

* removing dead code

* wip

* moved data encryption into the state machine

* restoring nym-lp tests

* update lp api model

* Add receiver index derivation

* Add receiver index derivation

* use derived receiver index

* feat: add kem key generation to nodes

* generate fresh x25519, mlkem768 and mceliece keys on config migration

* add lp peer config

* nym-node startup cleanup

* removed dependency on pre-rand09 from nym-lp

* re-expose LP information on the http API

* fixed tests compilation

* add peer config happy path tests

* formatting

* add more tests and fix bug

* better docs

* clippy and formatting issues

* return error on mceliece within NestedSession

* wasm fixes

* removed legacy nym-vpn-lib-wasm

* fixing wasm for real this time

* additional fixes

* add payload to kkt

* make clippy happy

* moved LP to nym-node crate

* cargo fmt

* integrate lpconfig payload

* fix response size trait impl

* Migrate receiver index

* Change receiver index to u32 and regorganize crates

* clippy

* hopefully final wasm fixes

* simple conversion method from semver to ciphersuite

* updated nym-node config template

* chore: remove duplicated code

---------

Co-authored-by: Georgio Nicolas <me@georgio.xyz>
2026-02-27 13:49:08 +00:00

472 lines
18 KiB
Rust

// Copyright 2023-2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::api::v1::node::models::{
LegacyHostInformationV1, LegacyHostInformationV2, LegacyHostInformationV3,
};
use crate::error::Error;
use nym_crypto::asymmetric::ed25519;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::ops::Deref;
#[cfg(feature = "client")]
pub mod client;
pub mod v1;
#[cfg(feature = "client")]
pub use client::Client;
// create the type alias manually if openapi is not enabled
pub type SignedHostInformation = SignedData<crate::api::v1::node::models::HostInformation>;
pub type SignedLewesProtocol = SignedData<crate::api::v1::lewes_protocol::models::LewesProtocol>;
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct SignedDataHostInfo {
// #[serde(flatten)]
pub data: crate::api::v1::node::models::HostInformation,
pub signature: String,
}
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct SignedLewesProtocolInfo {
// #[serde(flatten)]
pub data: crate::api::v1::lewes_protocol::models::LewesProtocol,
pub signature: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedData<T> {
// #[serde(flatten)]
pub data: T,
#[serde(with = "ed25519::bs58_ed25519_signature")]
pub signature: ed25519::Signature,
}
impl<T> SignedData<T> {
pub fn new(data: T, key: &ed25519::PrivateKey) -> Result<Self, Error>
where
T: Serialize,
{
let plaintext = serde_json::to_string(&data)?;
let signature = key.sign(plaintext);
Ok(SignedData { data, signature })
}
pub fn verify(&self, key: &ed25519::PublicKey) -> bool
where
T: Serialize,
{
let Ok(plaintext) = serde_json::to_string(&self.data) else {
return false;
};
key.verify(plaintext, &self.signature).is_ok()
}
}
impl SignedHostInformation {
pub fn verify_host_information(&self) -> bool {
if self.verify(&self.keys.ed25519_identity) {
return true;
}
// TODO: @JS: to remove downgrade support in future release(s)
let legacy_v3 = SignedData {
data: LegacyHostInformationV3::from(self.data.clone()),
signature: self.signature,
};
if legacy_v3.verify(&self.keys.ed25519_identity) {
return true;
}
// attempt to verify legacy signatures
let legacy_v3 = SignedData {
data: LegacyHostInformationV3::from(self.data.clone()),
signature: self.signature,
};
if legacy_v3.verify(&self.keys.ed25519_identity) {
return true;
}
let legacy_v2 = SignedData {
data: LegacyHostInformationV2::from(legacy_v3.data),
signature: self.signature,
};
if legacy_v2.verify(&self.keys.ed25519_identity) {
return true;
}
SignedData {
data: LegacyHostInformationV1::from(legacy_v2.data),
signature: self.signature,
}
.verify(&self.keys.ed25519_identity)
}
}
impl<T> Deref for SignedData<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.data
}
}
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
pub struct ErrorResponse {
pub message: String,
}
impl Display for ErrorResponse {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.message.fmt(f)
}
}
#[allow(deprecated)]
#[cfg(test)]
mod tests {
use super::*;
use crate::api::v1::node::models::{HostKeys, SphinxKey};
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_noise_keys::{NoiseVersion, VersionedNoiseKeyV1};
use nym_test_utils::helpers::deterministic_rng;
#[test]
fn dummy_signed_host_verification() {
let mut rng = deterministic_rng();
let ed22519 = ed25519::KeyPair::new(&mut rng);
let x25519_sphinx = x25519::KeyPair::new(&mut rng);
let x25519_sphinx2 = x25519::KeyPair::new(&mut rng);
let x25519_versioned_noise = VersionedNoiseKeyV1 {
supported_version: NoiseVersion::V1,
x25519_pubkey: *x25519::KeyPair::new(&mut rng).public_key(),
};
let current_rotation_id = 1234;
// no pre-announced keys
let host_info = crate::api::v1::node::models::HostInformation {
ip_address: vec!["1.1.1.1".parse().unwrap()],
hostname: Some("foomp.com".to_string()),
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: *ed22519.public_key(),
x25519_sphinx: *x25519_sphinx.public_key(),
primary_x25519_sphinx_key: SphinxKey {
rotation_id: current_rotation_id,
public_key: *x25519_sphinx.public_key(),
},
pre_announced_x25519_sphinx_key: None,
x25519_versioned_noise: None,
},
};
let signed_info = SignedHostInformation::new(host_info, ed22519.private_key()).unwrap();
assert!(signed_info.verify(ed22519.public_key()));
assert!(signed_info.verify_host_information());
let host_info_with_noise = crate::api::v1::node::models::HostInformation {
ip_address: vec!["1.1.1.1".parse().unwrap()],
hostname: Some("foomp.com".to_string()),
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: *ed22519.public_key(),
x25519_sphinx: *x25519_sphinx.public_key(),
primary_x25519_sphinx_key: SphinxKey {
rotation_id: current_rotation_id,
public_key: *x25519_sphinx.public_key(),
},
pre_announced_x25519_sphinx_key: None,
x25519_versioned_noise: Some(x25519_versioned_noise),
},
};
let signed_info =
SignedHostInformation::new(host_info_with_noise, ed22519.private_key()).unwrap();
assert!(signed_info.verify(ed22519.public_key()));
assert!(signed_info.verify_host_information());
// with pre-announced keys
let host_info = crate::api::v1::node::models::HostInformation {
ip_address: vec!["1.1.1.1".parse().unwrap()],
hostname: Some("foomp.com".to_string()),
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: *ed22519.public_key(),
x25519_sphinx: *x25519_sphinx.public_key(),
primary_x25519_sphinx_key: SphinxKey {
rotation_id: current_rotation_id,
public_key: *x25519_sphinx.public_key(),
},
pre_announced_x25519_sphinx_key: Some(SphinxKey {
rotation_id: current_rotation_id + 1,
public_key: *x25519_sphinx2.public_key(),
}),
x25519_versioned_noise: None,
},
};
let signed_info = SignedHostInformation::new(host_info, ed22519.private_key()).unwrap();
assert!(signed_info.verify(ed22519.public_key()));
assert!(signed_info.verify_host_information());
let host_info_with_noise = crate::api::v1::node::models::HostInformation {
ip_address: vec!["1.1.1.1".parse().unwrap()],
hostname: Some("foomp.com".to_string()),
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: *ed22519.public_key(),
x25519_sphinx: *x25519_sphinx.public_key(),
primary_x25519_sphinx_key: SphinxKey {
rotation_id: current_rotation_id,
public_key: *x25519_sphinx.public_key(),
},
pre_announced_x25519_sphinx_key: Some(SphinxKey {
rotation_id: current_rotation_id + 1,
public_key: *x25519_sphinx2.public_key(),
}),
x25519_versioned_noise: Some(x25519_versioned_noise),
},
};
let signed_info =
SignedHostInformation::new(host_info_with_noise, ed22519.private_key()).unwrap();
assert!(signed_info.verify(ed22519.public_key()));
assert!(signed_info.verify_host_information());
}
#[test]
fn dummy_legacy_v3_signed_host_verification() {
let mut rng = deterministic_rng();
let ed22519 = ed25519::KeyPair::new(&mut rng);
let x25519_sphinx = x25519::KeyPair::new(&mut rng);
let x25519_noise = x25519::KeyPair::new(&mut rng);
let legacy_info_no_noise = crate::api::v1::node::models::LegacyHostInformationV3 {
ip_address: vec!["1.1.1.1".parse().unwrap()],
hostname: Some("foomp.com".to_string()),
keys: crate::api::v1::node::models::LegacyHostKeysV3 {
ed25519_identity: *ed22519.public_key(),
x25519_sphinx: *x25519_sphinx.public_key(),
x25519_noise: None,
},
};
// note the usage of u32::max rotation id (as that's what the legacy data would be deserialised into)
let current_struct = crate::api::v1::node::models::HostInformation {
ip_address: vec!["1.1.1.1".parse().unwrap()],
hostname: Some("foomp.com".to_string()),
keys: HostKeys {
ed25519_identity: *ed22519.public_key(),
x25519_sphinx: *x25519_sphinx.public_key(),
primary_x25519_sphinx_key: SphinxKey {
rotation_id: u32::MAX,
public_key: *x25519_sphinx.public_key(),
},
pre_announced_x25519_sphinx_key: None,
x25519_versioned_noise: None,
},
};
// signature on legacy data
let signature = SignedData::new(legacy_info_no_noise, ed22519.private_key())
.unwrap()
.signature;
// signed blob with the 'current' structure
let current_struct = SignedData {
data: current_struct,
signature,
};
assert!(!current_struct.verify(ed22519.public_key()));
assert!(current_struct.verify_host_information());
// //technically this variant should never happen
let legacy_info_noise = crate::api::v1::node::models::LegacyHostInformationV3 {
ip_address: vec!["1.1.1.1".parse().unwrap()],
hostname: Some("foomp.com".to_string()),
keys: crate::api::v1::node::models::LegacyHostKeysV3 {
ed25519_identity: *ed22519.public_key(),
x25519_sphinx: *x25519_sphinx.public_key(),
x25519_noise: Some(*x25519_noise.public_key()),
},
};
// note the usage of u32::max rotation id (as that's what the legacy data would be deserialised into)
let current_struct_noise = crate::api::v1::node::models::HostInformation {
ip_address: vec!["1.1.1.1".parse().unwrap()],
hostname: Some("foomp.com".to_string()),
keys: HostKeys {
ed25519_identity: *ed22519.public_key(),
x25519_sphinx: *x25519_sphinx.public_key(),
primary_x25519_sphinx_key: SphinxKey {
rotation_id: u32::MAX,
public_key: *x25519_sphinx.public_key(),
},
pre_announced_x25519_sphinx_key: None,
x25519_versioned_noise: Some(VersionedNoiseKeyV1 {
supported_version: NoiseVersion::V1,
x25519_pubkey: legacy_info_noise.keys.x25519_noise.unwrap(),
}),
},
};
// signature on legacy data
let signature_noise = SignedData::new(legacy_info_noise, ed22519.private_key())
.unwrap()
.signature;
// signed blob with the 'current' structure
let current_struct_noise = SignedData {
data: current_struct_noise,
signature: signature_noise,
};
assert!(!current_struct_noise.verify(ed22519.public_key()));
assert!(current_struct_noise.verify_host_information())
}
#[test]
fn dummy_legacy_v2_signed_host_verification() {
let mut rng = deterministic_rng();
let ed22519 = ed25519::KeyPair::new(&mut rng);
let x25519_sphinx = x25519::KeyPair::new(&mut rng);
let x25519_noise = x25519::KeyPair::new(&mut rng);
let legacy_info_no_noise = crate::api::v1::node::models::LegacyHostInformationV2 {
ip_address: vec!["1.1.1.1".parse().unwrap()],
hostname: Some("foomp.com".to_string()),
keys: crate::api::v1::node::models::LegacyHostKeysV2 {
ed25519_identity: ed22519.public_key().to_base58_string(),
x25519_sphinx: x25519_sphinx.public_key().to_base58_string(),
x25519_noise: "".to_string(),
},
};
let legacy_info_noise = crate::api::v1::node::models::LegacyHostInformationV2 {
ip_address: vec!["1.1.1.1".parse().unwrap()],
hostname: Some("foomp.com".to_string()),
keys: crate::api::v1::node::models::LegacyHostKeysV2 {
ed25519_identity: ed22519.public_key().to_base58_string(),
x25519_sphinx: x25519_sphinx.public_key().to_base58_string(),
x25519_noise: x25519_noise.public_key().to_base58_string(),
},
};
// note the usage of u32::max rotation id (as that's what the legacy data would be deserialised into)
let host_info_no_noise = crate::api::v1::node::models::HostInformation {
ip_address: legacy_info_no_noise.ip_address.clone(),
hostname: legacy_info_no_noise.hostname.clone(),
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: legacy_info_no_noise.keys.ed25519_identity.parse().unwrap(),
x25519_sphinx: *x25519_sphinx.public_key(),
primary_x25519_sphinx_key: SphinxKey {
rotation_id: u32::MAX,
public_key: *x25519_sphinx.public_key(),
},
pre_announced_x25519_sphinx_key: None,
x25519_versioned_noise: None,
},
};
// note the usage of u32::max rotation id (as that's what the legacy data would be deserialised into)
let host_info_noise = crate::api::v1::node::models::HostInformation {
ip_address: legacy_info_noise.ip_address.clone(),
hostname: legacy_info_noise.hostname.clone(),
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: legacy_info_noise.keys.ed25519_identity.parse().unwrap(),
x25519_sphinx: *x25519_sphinx.public_key(),
primary_x25519_sphinx_key: SphinxKey {
rotation_id: u32::MAX,
public_key: *x25519_sphinx.public_key(),
},
pre_announced_x25519_sphinx_key: None,
x25519_versioned_noise: Some(VersionedNoiseKeyV1 {
supported_version: NoiseVersion::V1,
x25519_pubkey: legacy_info_noise.keys.x25519_noise.parse().unwrap(),
}),
},
};
// signature on legacy data
let signature_no_noise = SignedData::new(legacy_info_no_noise, ed22519.private_key())
.unwrap()
.signature;
let signature_noise = SignedData::new(legacy_info_noise, ed22519.private_key())
.unwrap()
.signature;
// signed blob with the 'current' structure
let current_struct_no_noise = SignedData {
data: host_info_no_noise,
signature: signature_no_noise,
};
let current_struct_noise = SignedData {
data: host_info_noise,
signature: signature_noise,
};
assert!(!current_struct_no_noise.verify(ed22519.public_key()));
assert!(current_struct_no_noise.verify_host_information());
assert!(!current_struct_noise.verify(ed22519.public_key()));
assert!(current_struct_noise.verify_host_information())
}
#[test]
fn dummy_legacy_v1_signed_host_verification() {
let mut rng = deterministic_rng();
let ed22519 = ed25519::KeyPair::new(&mut rng);
let x25519_sphinx = x25519::KeyPair::new(&mut rng);
let legacy_info = crate::api::v1::node::models::LegacyHostInformationV1 {
ip_address: vec!["1.1.1.1".parse().unwrap()],
hostname: Some("foomp.com".to_string()),
keys: crate::api::v1::node::models::LegacyHostKeysV1 {
ed25519: ed22519.public_key().to_base58_string(),
x25519: x25519_sphinx.public_key().to_base58_string(),
},
};
// note the usage of u32::max rotation id (as that's what the legacy data would be deserialised into)
let host_info = crate::api::v1::node::models::HostInformation {
ip_address: legacy_info.ip_address.clone(),
hostname: legacy_info.hostname.clone(),
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: legacy_info.keys.ed25519.parse().unwrap(),
x25519_sphinx: *x25519_sphinx.public_key(),
primary_x25519_sphinx_key: SphinxKey {
rotation_id: u32::MAX,
public_key: *x25519_sphinx.public_key(),
},
pre_announced_x25519_sphinx_key: None,
x25519_versioned_noise: None,
},
};
// signature on legacy data
let signature = SignedData::new(legacy_info, ed22519.private_key())
.unwrap()
.signature;
// signed blob with the 'current' structure
let current_struct = SignedData {
data: host_info,
signature,
};
assert!(!current_struct.verify(ed22519.public_key()));
assert!(current_struct.verify_host_information())
}
}