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>
This commit is contained in:
Jędrzej Stuczyński
2026-02-27 13:49:08 +00:00
committed by GitHub
parent e5c3f39a57
commit f6bd511599
167 changed files with 10977 additions and 16911 deletions
+1
View File
@@ -22,6 +22,7 @@ hex.workspace = true
tracing.workspace = true
pnet_packet.workspace = true
rand.workspace = true
rand09.workspace = true
reqwest = { workspace = true, features = ["socks"] }
serde.workspace = true
serde_json.workspace = true
-10
View File
@@ -1,12 +1,9 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::common::nodes::TestedNodeLpDetails;
use nym_crypto::asymmetric::ed25519;
use nym_ip_packet_requests::v8::response::{
ControlResponse, DataResponse, InfoLevel, IpPacketResponse, IpPacketResponseData,
};
use nym_lp::peer::LpRemotePeer;
use nym_sdk::{
DebugConfig, NymApiTopologyProvider, NymApiTopologyProviderConfig, NymNetworkDetails,
TopologyProvider, mixnet::ReconstructedMessage,
@@ -15,13 +12,6 @@ use nym_topology::NymTopology;
use tracing::*;
use url::Url;
pub fn to_lp_remote_peer(identity: ed25519::PublicKey, data: TestedNodeLpDetails) -> LpRemotePeer {
LpRemotePeer::new(identity, data.x25519).with_key_digests(
data.expected_kem_key_hashes,
data.expected_signing_key_hashes,
)
}
pub fn mixnet_debug_config(
min_gateway_performance: Option<u8>,
ignore_egress_epoch_role: bool,
+38 -32
View File
@@ -3,25 +3,25 @@
use anyhow::{Context, anyhow, bail};
use nym_api_requests::models::{
AuthenticatorDetailsV1, DeclaredRolesV1, DescribedNodeTypeV1, HostInformationV1,
IpPacketRouterDetailsV1, NetworkRequesterDetailsV1, NymNodeDataV1,
OffsetDateTimeJsonSchemaWrapper, WebSocketsV1, WireguardDetailsV1,
AuthenticatorDetailsV2, DeclaredRolesV2, DescribedNodeTypeV2, HostInformationV2,
IpPacketRouterDetailsV2, NetworkRequesterDetailsV2, NymNodeDataV2,
OffsetDateTimeJsonSchemaWrapper, WebSocketsV2, WireguardDetailsV2,
};
use nym_authenticator_requests::AuthenticatorVersion;
use nym_bin_common::build_information::BinaryBuildInformationOwned;
use nym_crypto::asymmetric::x25519;
use nym_http_api_client::UserAgent;
use nym_kkt_ciphersuite::SignatureScheme;
use nym_kkt_ciphersuite::Ciphersuite;
use nym_kkt_ciphersuite::{KEM, KEMKeyDigests};
use nym_lp::peer::{DHPublicKey, LpRemotePeer};
use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT;
use nym_node_requests::api::client::NymNodeApiClientExt;
use nym_node_requests::api::v1::node::models::AuxiliaryDetails as NodeAuxiliaryDetails;
use nym_sdk::mixnet::NodeIdentity;
use nym_sdk::mixnet::Recipient;
use nym_validator_client::client::NymApiClientExt;
use nym_validator_client::models::NymNodeDescriptionV1;
use nym_validator_client::models::NymNodeDescriptionV2;
use rand::prelude::IteratorRandom;
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use std::net::{IpAddr, SocketAddr};
use std::time::Duration;
use time::OffsetDateTime;
@@ -90,7 +90,7 @@ use url::Url;
#[derive(Clone)]
pub struct DirectoryNode {
described: NymNodeDescriptionV1,
described: NymNodeDescriptionV2,
}
impl DirectoryNode {
@@ -237,11 +237,11 @@ pub async fn query_gateway_by_ip(address: String) -> anyhow::Result<DirectoryNod
.await
.inspect_err(|e| error!("Failed to get wireguard information : {e}"))
.ok();
// let lp_result = client
// .get_lewes_protocol()
// .await
// .inspect_err(|e| error!("Failed to get LP information : {e}"))
// .ok();
let lp_result = client
.get_lewes_protocol()
.await
.inspect_err(|e| error!("Failed to get LP information : {e}"))
.ok();
// Check required fields
let host_info = host_info_result.context("Failed to get host information")?;
@@ -261,22 +261,22 @@ pub async fn query_gateway_by_ip(address: String) -> anyhow::Result<DirectoryNod
}
// Convert to our internal types
let network_requester: Option<NetworkRequesterDetailsV1> =
nr_result.map(|nr| NetworkRequesterDetailsV1 {
let network_requester: Option<NetworkRequesterDetailsV2> =
nr_result.map(|nr| NetworkRequesterDetailsV2 {
address: nr.address,
uses_exit_policy: false, // Field not availabe, to change if it becomes useful here
});
let ip_packet_router: Option<IpPacketRouterDetailsV1> =
ipr_result.map(|ipr| IpPacketRouterDetailsV1 {
let ip_packet_router: Option<IpPacketRouterDetailsV2> =
ipr_result.map(|ipr| IpPacketRouterDetailsV2 {
address: ipr.address,
});
let authenticator: Option<AuthenticatorDetailsV1> =
authenticator_result.map(|auth| AuthenticatorDetailsV1 {
let authenticator: Option<AuthenticatorDetailsV2> =
authenticator_result.map(|auth| AuthenticatorDetailsV2 {
address: auth.address,
});
#[allow(deprecated)]
let wireguard: Option<WireguardDetailsV1> =
wireguard_result.map(|wg| WireguardDetailsV1 {
let wireguard: Option<WireguardDetailsV2> =
wireguard_result.map(|wg| WireguardDetailsV2 {
port: wg.tunnel_port, // Use tunnel_port for deprecated port field
tunnel_port: wg.tunnel_port,
metadata_port: wg.metadata_port,
@@ -284,14 +284,14 @@ pub async fn query_gateway_by_ip(address: String) -> anyhow::Result<DirectoryNod
});
// Construct NymNodeData
let node_data = NymNodeDataV1 {
let node_data = NymNodeDataV2 {
last_polled: OffsetDateTimeJsonSchemaWrapper(OffsetDateTime::now_utc()),
host_information: HostInformationV1 {
host_information: HostInformationV2 {
ip_address: host_info.data.ip_address,
hostname: host_info.data.hostname,
keys: host_info.data.keys.into(),
},
declared_role: DeclaredRolesV1 {
declared_role: DeclaredRolesV2 {
mixnode: roles.mixnode_enabled,
entry: roles.gateway_enabled,
exit_nr: roles.network_requester_enabled,
@@ -314,17 +314,17 @@ pub async fn query_gateway_by_ip(address: String) -> anyhow::Result<DirectoryNod
ip_packet_router,
authenticator,
wireguard,
// lewes_protocol: lp_result.map(Into::into),
mixnet_websockets: WebSocketsV1 {
lewes_protocol: lp_result.map(Into::into),
mixnet_websockets: WebSocketsV2 {
ws_port: websockets.ws_port,
wss_port: websockets.wss_port,
},
};
// Create NymNodeDescription
let described = NymNodeDescriptionV1 {
let described = NymNodeDescriptionV2 {
node_id: 0, // We don't have a node_id from direct query
contract_node_type: DescribedNodeTypeV1::NymNode, // All new nodes are NymNode type
contract_node_type: DescribedNodeTypeV2::NymNode, // All new nodes are NymNode type
description: node_data,
};
@@ -361,7 +361,7 @@ impl NymApiDirectory {
debug!("Fetching all described nodes from nym-api...");
let described_nodes = api_client
.get_all_described_nodes()
.get_all_described_nodes_v2()
.await
.context("nym api query failure")?;
@@ -482,8 +482,14 @@ pub struct TestedNodeDetails {
#[derive(Debug, Clone)]
pub struct TestedNodeLpDetails {
pub address: SocketAddr,
pub expected_kem_key_hashes: HashMap<KEM, KEMKeyDigests>,
pub expected_signing_key_hashes: HashMap<SignatureScheme, KEMKeyDigests>,
pub x25519: x25519::PublicKey,
pub expected_kem_key_hashes: BTreeMap<KEM, KEMKeyDigests>,
pub x25519: DHPublicKey,
pub lp_version: u8,
pub ciphersuite: Ciphersuite,
}
impl TestedNodeLpDetails {
pub fn into_remote_peer(self) -> LpRemotePeer {
LpRemotePeer::new(self.x25519).with_key_digests(self.expected_kem_key_hashes)
}
}
+37 -27
View File
@@ -28,10 +28,12 @@ use nym_credentials_interface::{CredentialSpendingData, TicketType};
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_ip_packet_client::IprClientConnect;
use nym_ip_packet_requests::{IpPair, codec::MultiIpPacketCodec};
use nym_lp::peer::DHKeyPair;
use nym_registration_client::{LpRegistrationClient, NestedLpSession};
use nym_sdk::NymNetworkDetails;
use nym_sdk::mixnet::{MixnetClient, MixnetClientBuilder, NodeIdentity, Recipient, Socks5};
use nym_topology::{HardcodedTopologyProvider, NymTopology};
use rand09::SeedableRng;
use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr},
sync::Arc,
@@ -163,23 +165,25 @@ pub async fn lp_registration_probe(
) -> anyhow::Result<LpProbeResults> {
let lp_address = gateway_lp_data.address;
let lp_version = gateway_lp_data.lp_version;
let peer = helpers::to_lp_remote_peer(gateway_identity, gateway_lp_data);
let lp_ciphersuite = gateway_lp_data.ciphersuite;
let peer = gateway_lp_data.into_remote_peer();
info!("Starting LP registration probe for gateway at {lp_address}");
let mut lp_outcome = LpProbeResults::default();
// Generate Ed25519 keypair for this connection (X25519 will be derived internally by LP)
let mut rng = rand::thread_rng();
let client_ed25519_keypair = std::sync::Arc::new(ed25519::KeyPair::new(&mut rng));
let mut rng09 = rand09::rngs::StdRng::from_os_rng();
let client_x25519_keypair = Arc::new(DHKeyPair::new(&mut rng09));
// Step 0: Derive X25519 keys from Ed25519 for the gateways
// Create LP registration client (uses Ed25519 keys directly, derives X25519 internally)
let mut client = LpRegistrationClient::<TcpStream>::new_with_default_config(
client_ed25519_keypair,
client_x25519_keypair,
peer,
lp_address,
lp_ciphersuite,
lp_version,
);
@@ -209,23 +213,13 @@ pub async fn lp_registration_probe(
let wg_keypair = nym_crypto::asymmetric::x25519::KeyPair::new(&mut rng);
// Convert gateway identity to ed25519 public key
let gateway_ed25519_pubkey = match nym_crypto::asymmetric::ed25519::PublicKey::from_bytes(
&gateway_identity.to_bytes(),
) {
Ok(key) => key,
Err(e) => {
let error_msg = format!("Failed to convert gateway identity: {}", e);
error!("{}", error_msg);
lp_outcome.error = Some(error_msg);
return Ok(lp_outcome);
}
};
let gateway_ed25519_pubkey = gateway_identity;
// Register using the new packet-per-connection API (returns GatewayData directly)
let ticket_type = TicketType::V1WireguardEntry;
let gateway_data = match client
.register_dvpn(
&mut rng,
&mut rng09,
&wg_keypair,
&gateway_ed25519_pubkey,
bandwidth_controller,
@@ -299,21 +293,26 @@ pub async fn wg_probe_lp(
let entry_lp_version = entry_lp_data.lp_version;
let exit_lp_version = exit_lp_data.lp_version;
let entry_lp_ciphersuite = entry_lp_data.ciphersuite;
let exit_lp_ciphersuite = exit_lp_data.ciphersuite;
info!("Starting LP-based WireGuard probe (entry→exit via forwarding)");
let mut wg_outcome = WgProbeResults::default();
// Generate Ed25519 keypairs for LP protocol
let mut rng = rand::thread_rng();
let entry_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut rng));
let exit_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut rng));
// Generate x25519 keypairs for LP protocol
let mut rng09 = rand09::rngs::StdRng::from_os_rng();
let entry_lp_keypair = Arc::new(DHKeyPair::new(&mut rng09));
let exit_lp_keypair = Arc::new(DHKeyPair::new(&mut rng09));
// Generate WireGuard keypairs for VPN registration
let mut rng = rand::rngs::OsRng;
let entry_wg_keypair = x25519::KeyPair::new(&mut rng);
let exit_wg_keypair = x25519::KeyPair::new(&mut rng);
let entry_peer = helpers::to_lp_remote_peer(entry_gateway.identity, entry_lp_data);
let exit_peer = helpers::to_lp_remote_peer(exit_gateway.identity, exit_lp_data);
let entry_peer = entry_lp_data.into_remote_peer();
let exit_peer = exit_lp_data.into_remote_peer();
// STEP 1: Establish outer LP session with entry gateway
// LpRegistrationClient uses packet-per-connection model - connect() is gone,
@@ -323,6 +322,7 @@ pub async fn wg_probe_lp(
entry_lp_keypair,
entry_peer,
entry_address,
entry_lp_ciphersuite,
entry_lp_version,
);
@@ -335,16 +335,26 @@ pub async fn wg_probe_lp(
// STEP 2: Use nested session to register with exit gateway via forwarding
info!("Registering with exit gateway via entry forwarding...");
let mut nested_session =
NestedLpSession::new(exit_address, exit_lp_keypair, exit_peer, exit_lp_version);
let mut nested_session = NestedLpSession::new(
exit_address,
exit_lp_keypair,
exit_peer,
exit_lp_ciphersuite,
exit_lp_version,
);
let exit_gateway_pubkey = exit_gateway.identity;
// Perform handshake and registration with exit gateway via forwarding
if let Err(err) = nested_session.perform_handshake(&mut entry_client).await {
error!("Failed to perform handshake with exit gateway: {err}");
return Ok(wg_outcome);
};
let exit_gateway_data = match nested_session
.handshake_and_register_dvpn(
.register_dvpn(
&mut entry_client,
&mut rng,
&mut rng09,
&exit_wg_keypair,
&exit_gateway_pubkey,
bandwidth_controller,
@@ -370,7 +380,7 @@ pub async fn wg_probe_lp(
// Use packet-per-connection register() which returns GatewayData directly
let entry_gateway_data = match entry_client
.register_dvpn(
&mut rng,
&mut rng09,
&entry_wg_keypair,
&entry_gateway_pubkey,
bandwidth_controller,