f6bd511599
* 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>
472 lines
18 KiB
Rust
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())
|
|
}
|
|
}
|