From bb5b2eafcf0eba9c97968521bfa1612f681ffc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 5 Mar 2025 15:02:07 +0100 Subject: [PATCH 01/18] Allow IPR reconnect to session (#5562) --- .../src/clients/connected_clients.rs | 35 ++++++++++--------- .../ip-packet-router/src/messages/response.rs | 3 -- .../src/messages/response/v6.rs | 3 -- .../src/messages/response/v7.rs | 3 -- .../src/messages/response/v8.rs | 3 -- .../ip-packet-router/src/mixnet_listener.rs | 5 +-- 6 files changed, 21 insertions(+), 31 deletions(-) diff --git a/service-providers/ip-packet-router/src/clients/connected_clients.rs b/service-providers/ip-packet-router/src/clients/connected_clients.rs index 1957ddaa15..853c83f884 100644 --- a/service-providers/ip-packet-router/src/clients/connected_clients.rs +++ b/service-providers/ip-packet-router/src/clients/connected_clients.rs @@ -59,23 +59,24 @@ impl ConnectedClients { .any(|client| client.client_id == *client_id) } - //fn lookup_ip_from_client_id(&self, client_id: &ConnectedClientId) -> Option { - // self.clients_ipv4_mapping - // .iter() - // .find_map(|(ipv4, connected_client)| { - // if connected_client.client_id == *client_id { - // Some(IpPair::new(*ipv4, connected_client.ipv6)) - // } else { - // None - // } - // }) - //} - // - //fn lookup_client(&self, client_id: &ConnectedClientId) -> Option<&ConnectedClient> { - // self.clients_ipv4_mapping - // .values() - // .find(|connected_client| connected_client.client_id == *client_id) - //} + pub(crate) fn lookup_ip_from_client_id(&self, client_id: &ConnectedClientId) -> Option { + self.clients_ipv4_mapping + .iter() + .find_map(|(ipv4, connected_client)| { + if connected_client.client_id == *client_id { + Some(IpPair::new(*ipv4, connected_client.ipv6)) + } else { + None + } + }) + } + + #[allow(unused)] + fn lookup_client(&self, client_id: &ConnectedClientId) -> Option<&ConnectedClient> { + self.clients_ipv4_mapping + .values() + .find(|connected_client| connected_client.client_id == *client_id) + } pub(crate) fn connect( &mut self, diff --git a/service-providers/ip-packet-router/src/messages/response.rs b/service-providers/ip-packet-router/src/messages/response.rs index 6e60d41433..d1bf0d08b4 100644 --- a/service-providers/ip-packet-router/src/messages/response.rs +++ b/service-providers/ip-packet-router/src/messages/response.rs @@ -90,9 +90,6 @@ pub(crate) struct DynamicConnectSuccess { #[derive(Clone, Debug, thiserror::Error)] pub(crate) enum DynamicConnectFailureReason { - #[error("client already connected")] - ClientAlreadyConnected, - #[error("no available ip address")] NoAvailableIp, diff --git a/service-providers/ip-packet-router/src/messages/response/v6.rs b/service-providers/ip-packet-router/src/messages/response/v6.rs index 290f78971b..bf7fc9dbdf 100644 --- a/service-providers/ip-packet-router/src/messages/response/v6.rs +++ b/service-providers/ip-packet-router/src/messages/response/v6.rs @@ -119,9 +119,6 @@ impl From for DynamicConnectResponseReplyV6 { impl From for DynamicConnectFailureReasonV6 { fn from(reason: DynamicConnectFailureReason) -> Self { match reason { - DynamicConnectFailureReason::ClientAlreadyConnected => { - DynamicConnectFailureReasonV6::RequestedNymAddressAlreadyInUse - } DynamicConnectFailureReason::NoAvailableIp => { DynamicConnectFailureReasonV6::NoAvailableIp } diff --git a/service-providers/ip-packet-router/src/messages/response/v7.rs b/service-providers/ip-packet-router/src/messages/response/v7.rs index 60a3aa6d8a..f2ad4e849f 100644 --- a/service-providers/ip-packet-router/src/messages/response/v7.rs +++ b/service-providers/ip-packet-router/src/messages/response/v7.rs @@ -119,9 +119,6 @@ impl From for DynamicConnectResponseReplyV7 { impl From for DynamicConnectFailureReasonV7 { fn from(reason: DynamicConnectFailureReason) -> Self { match reason { - DynamicConnectFailureReason::ClientAlreadyConnected => { - DynamicConnectFailureReasonV7::RequestedNymAddressAlreadyInUse - } DynamicConnectFailureReason::NoAvailableIp => { DynamicConnectFailureReasonV7::NoAvailableIp } diff --git a/service-providers/ip-packet-router/src/messages/response/v8.rs b/service-providers/ip-packet-router/src/messages/response/v8.rs index 3f73479a05..99b87075c8 100644 --- a/service-providers/ip-packet-router/src/messages/response/v8.rs +++ b/service-providers/ip-packet-router/src/messages/response/v8.rs @@ -82,9 +82,6 @@ impl From for ConnectResponseReplyV8 { impl From for ConnectFailureReasonV8 { fn from(reason: DynamicConnectFailureReason) -> Self { match reason { - DynamicConnectFailureReason::ClientAlreadyConnected => { - ConnectFailureReasonV8::ClientAlreadyConnected - } DynamicConnectFailureReason::NoAvailableIp => ConnectFailureReasonV8::NoAvailableIp, DynamicConnectFailureReason::Other(err) => ConnectFailureReasonV8::Other(err), } diff --git a/service-providers/ip-packet-router/src/mixnet_listener.rs b/service-providers/ip-packet-router/src/mixnet_listener.rs index 293718831c..31d4b2e5fe 100644 --- a/service-providers/ip-packet-router/src/mixnet_listener.rs +++ b/service-providers/ip-packet-router/src/mixnet_listener.rs @@ -242,13 +242,14 @@ impl MixnetListener { .map(Duration::from_millis) .unwrap_or(nym_ip_packet_requests::codec::BUFFER_TIMEOUT); - if self.connected_clients.is_client_connected(&reply_to) { + if let Some(ips) = self.connected_clients.lookup_ip_from_client_id(&reply_to) { + log::debug!("Reconnecting to the previous session"); return Ok(Some(VersionedResponse { version, reply_to, response: Response::DynamicConnect { request_id, - reply: DynamicConnectFailureReason::ClientAlreadyConnected.into(), + reply: DynamicConnectSuccess { ips }.into(), }, })); } From 7785d085cfe57b4a5473e089ff75af6ba1d54075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 5 Mar 2025 15:17:51 +0100 Subject: [PATCH 02/18] Handle disconnect in IPR (#5547) * Implement disconnect in the IPR * Remove unused async --- .../ip-packet-router/src/clients/client_id.rs | 2 +- .../src/clients/connected_clients.rs | 36 ++++++----- .../ip-packet-router/src/mixnet_listener.rs | 59 +++++++++++++++---- .../ip-packet-router/src/tun_listener.rs | 2 +- 4 files changed, 69 insertions(+), 30 deletions(-) diff --git a/service-providers/ip-packet-router/src/clients/client_id.rs b/service-providers/ip-packet-router/src/clients/client_id.rs index 5add99a724..2842f261d3 100644 --- a/service-providers/ip-packet-router/src/clients/client_id.rs +++ b/service-providers/ip-packet-router/src/clients/client_id.rs @@ -7,7 +7,7 @@ use nym_sdk::mixnet::{AnonymousSenderTag, Recipient}; use crate::error::{IpPacketRouterError, Result}; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub(crate) enum ConnectedClientId { AnonymousSenderTag(AnonymousSenderTag), NymAddress(Box), diff --git a/service-providers/ip-packet-router/src/clients/connected_clients.rs b/service-providers/ip-packet-router/src/clients/connected_clients.rs index 853c83f884..7100048dd8 100644 --- a/service-providers/ip-packet-router/src/clients/connected_clients.rs +++ b/service-providers/ip-packet-router/src/clients/connected_clients.rs @@ -59,6 +59,24 @@ impl ConnectedClients { .any(|client| client.client_id == *client_id) } + pub(crate) fn disconnect_client(&mut self, client_id: &ConnectedClientId) { + if let Some(ips) = self.lookup_ip_from_client_id(client_id) { + log::debug!("Disconnect client that requested to do so: {ips}"); + self.disconnect_client_handle(ips); + } + } + + fn disconnect_client_handle(&mut self, ips: IpPair) { + self.clients_ipv4_mapping.remove(&ips.ipv4); + self.clients_ipv6_mapping.remove(&ips.ipv6); + self.tun_listener_connected_client_tx + .send(ConnectedClientEvent::Disconnect(DisconnectEvent(ips))) + .inspect_err(|err| { + log::error!("Failed to send disconnect event: {err}"); + }) + .ok(); + } + pub(crate) fn lookup_ip_from_client_id(&self, client_id: &ConnectedClientId) -> Option { self.clients_ipv4_mapping .iter() @@ -164,14 +182,7 @@ impl ConnectedClients { ) { for (ips, _) in &stopped_clients { log::info!("Disconnect stopped client: {ips}"); - self.clients_ipv4_mapping.remove(&ips.ipv4); - self.clients_ipv6_mapping.remove(&ips.ipv6); - self.tun_listener_connected_client_tx - .send(ConnectedClientEvent::Disconnect(DisconnectEvent(*ips))) - .inspect_err(|err| { - log::error!("Failed to send disconnect event: {err}"); - }) - .ok(); + self.disconnect_client_handle(*ips); } } @@ -181,14 +192,7 @@ impl ConnectedClients { ) { for (ips, _) in &inactive_clients { log::info!("Disconnect inactive client: {ips}"); - self.clients_ipv4_mapping.remove(&ips.ipv4); - self.clients_ipv6_mapping.remove(&ips.ipv6); - self.tun_listener_connected_client_tx - .send(ConnectedClientEvent::Disconnect(DisconnectEvent(*ips))) - .inspect_err(|err| { - log::error!("Failed to send disconnect event: {err}"); - }) - .ok(); + self.disconnect_client_handle(*ips); } } diff --git a/service-providers/ip-packet-router/src/mixnet_listener.rs b/service-providers/ip-packet-router/src/mixnet_listener.rs index 31d4b2e5fe..b0957236ac 100644 --- a/service-providers/ip-packet-router/src/mixnet_listener.rs +++ b/service-providers/ip-packet-router/src/mixnet_listener.rs @@ -22,9 +22,9 @@ use crate::{ IpPacketRequest, PingRequest, StaticConnectRequest, }, response::{ - DynamicConnectFailureReason, DynamicConnectSuccess, HealthResponse, InfoLevel, - InfoResponse, InfoResponseReply, Response, StaticConnectFailureReason, - StaticConnectResponse, VersionedResponse, + DisconnectFailureReason, DisconnectResponse, DynamicConnectFailureReason, + DynamicConnectSuccess, HealthResponse, InfoLevel, InfoResponse, InfoResponseReply, + Response, StaticConnectFailureReason, StaticConnectResponse, VersionedResponse, }, ClientVersion, }, @@ -225,7 +225,7 @@ impl MixnetListener { })) } - async fn on_dynamic_connect_request( + fn on_dynamic_connect_request( &mut self, connect_request: DynamicConnectRequest, ) -> PacketHandleResult { @@ -293,12 +293,47 @@ impl MixnetListener { })) } - fn on_disconnect_request(&self, _disconnect_request: DisconnectRequest) -> PacketHandleResult { - log::info!("Received disconnect request: not implemented, dropping"); - Ok(None) + fn on_disconnect_request( + &mut self, + disconnect_request: DisconnectRequest, + ) -> PacketHandleResult { + log::info!( + "Received disconnect request from {}", + disconnect_request.sent_by + ); + + let version = disconnect_request.version; + let request_id = disconnect_request.request_id; + let client_id = disconnect_request.sent_by; + + // Check if the client is connected + if !self.connected_clients.is_client_connected(&client_id) { + log::info!("Client {} is not connected, cannot disconnect", client_id); + return Ok(Some(VersionedResponse { + version, + reply_to: client_id, + response: Response::Disconnect { + request_id, + reply: DisconnectResponse::Failure(DisconnectFailureReason::ClientNotConnected), + }, + })); + } + + // Disconnect the client + log::info!("Disconnecting client {}", client_id); + self.connected_clients.disconnect_client(&client_id); + + Ok(Some(VersionedResponse { + version, + reply_to: client_id, + response: Response::Disconnect { + request_id, + reply: DisconnectResponse::Success, + }, + })) } - async fn on_ping_request(&self, ping_request: PingRequest) -> PacketHandleResult { + fn on_ping_request(&self, ping_request: PingRequest) -> PacketHandleResult { Ok(Some(VersionedResponse { version: ping_request.version, reply_to: ping_request.sent_by, @@ -308,7 +343,7 @@ impl MixnetListener { })) } - async fn on_health_request(&self, health_request: HealthRequest) -> PacketHandleResult { + fn on_health_request(&self, health_request: HealthRequest) -> PacketHandleResult { Ok(Some(VersionedResponse { version: health_request.version, reply_to: health_request.sent_by, @@ -325,10 +360,10 @@ impl MixnetListener { async fn on_control_request(&mut self, control_request: ControlRequest) -> PacketHandleResult { match control_request { ControlRequest::StaticConnect(r) => self.on_static_connect_request(r).await, - ControlRequest::DynamicConnect(r) => self.on_dynamic_connect_request(r).await, + ControlRequest::DynamicConnect(r) => self.on_dynamic_connect_request(r), ControlRequest::Disconnect(r) => self.on_disconnect_request(r), - ControlRequest::Ping(r) => self.on_ping_request(r).await, - ControlRequest::Health(r) => self.on_health_request(r).await, + ControlRequest::Ping(r) => self.on_ping_request(r), + ControlRequest::Health(r) => self.on_health_request(r), } } diff --git a/service-providers/ip-packet-router/src/tun_listener.rs b/service-providers/ip-packet-router/src/tun_listener.rs index 0d4a99e3b9..072ade7ab0 100644 --- a/service-providers/ip-packet-router/src/tun_listener.rs +++ b/service-providers/ip-packet-router/src/tun_listener.rs @@ -103,7 +103,7 @@ impl TunListener { .update(ConnectedClientEvent::Disconnect(DisconnectEvent(*ips))); } } else { - log::info!( + log::debug!( "dropping packet from network: no registered client for destination: {dst_addr}" ); } From c284b1e8b1c9b70636c09537750726dbe8c4c5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 5 Mar 2025 15:41:44 +0100 Subject: [PATCH 03/18] Create authenticator v5 request/response types (#5561) * Create authenticator v5 request/response types * Support v5 in the authenticator * Fix tests * Bump nym-node version --- Cargo.lock | 2 +- common/authenticator-requests/src/lib.rs | 5 +- common/authenticator-requests/src/traits.rs | 112 +++- .../src/v5/conversion.rs | 478 ++++++++++++++++++ common/authenticator-requests/src/v5/mod.rs | 10 + .../src/v5/registration.rs | 287 +++++++++++ .../authenticator-requests/src/v5/request.rs | 132 +++++ .../authenticator-requests/src/v5/response.rs | 132 +++++ common/authenticator-requests/src/v5/topup.rs | 15 + nym-node/Cargo.toml | 4 +- .../authenticator/src/cli/request.rs | 8 +- service-providers/authenticator/src/error.rs | 3 + .../authenticator/src/mixnet_listener.rs | 153 ++++-- 13 files changed, 1277 insertions(+), 64 deletions(-) create mode 100644 common/authenticator-requests/src/v5/conversion.rs create mode 100644 common/authenticator-requests/src/v5/mod.rs create mode 100644 common/authenticator-requests/src/v5/registration.rs create mode 100644 common/authenticator-requests/src/v5/request.rs create mode 100644 common/authenticator-requests/src/v5/response.rs create mode 100644 common/authenticator-requests/src/v5/topup.rs diff --git a/Cargo.lock b/Cargo.lock index 2aaf965a5d..9cce5fff5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6168,7 +6168,7 @@ dependencies = [ [[package]] name = "nym-node" -version = "1.6.0" +version = "1.6.1" dependencies = [ "anyhow", "arc-swap", diff --git a/common/authenticator-requests/src/lib.rs b/common/authenticator-requests/src/lib.rs index ed987a9a50..351a9b443c 100644 --- a/common/authenticator-requests/src/lib.rs +++ b/common/authenticator-requests/src/lib.rs @@ -6,14 +6,15 @@ pub mod v1; pub mod v2; pub mod v3; pub mod v4; +pub mod v5; mod error; mod util; pub use error::Error; -pub use v4 as latest; +pub use v5 as latest; -pub const CURRENT_VERSION: u8 = 4; +pub const CURRENT_VERSION: u8 = 5; fn make_bincode_serializer() -> impl bincode::Options { use bincode::Options; diff --git a/common/authenticator-requests/src/traits.rs b/common/authenticator-requests/src/traits.rs index 7c7064d174..baaaf8b422 100644 --- a/common/authenticator-requests/src/traits.rs +++ b/common/authenticator-requests/src/traits.rs @@ -8,8 +8,8 @@ use nym_sphinx::addressing::clients::Recipient; use nym_wireguard_types::PeerPublicKey; use crate::{ - v1, v2, v3, - v4::{self, registration::IpPair}, + v1, v2, v3, v4, + v5::{self, registration::IpPair}, Error, }; @@ -19,6 +19,7 @@ pub enum AuthenticatorVersion { V2, V3, V4, + V5, UNKNOWN, } @@ -34,6 +35,8 @@ impl From for AuthenticatorVersion { AuthenticatorVersion::V3 } else if value.version == v4::VERSION { AuthenticatorVersion::V4 + } else if value.version == v5::VERSION { + AuthenticatorVersion::V5 } else { AuthenticatorVersion::UNKNOWN } @@ -68,6 +71,12 @@ impl InitMessage for v4::registration::InitMessage { } } +impl InitMessage for v5::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + pub trait FinalMessage { fn pub_key(&self) -> PeerPublicKey; fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error>; @@ -138,6 +147,24 @@ impl FinalMessage for v4::registration::FinalMessage { self.gateway_client.verify(private_key, nonce) } + fn private_ips(&self) -> IpPair { + self.gateway_client.private_ips.into() + } + + fn credential(&self) -> Option { + self.credential.clone() + } +} + +impl FinalMessage for v5::registration::FinalMessage { + fn pub_key(&self) -> PeerPublicKey { + self.gateway_client.pub_key + } + + fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + self.gateway_client.verify(private_key, nonce) + } + fn private_ips(&self) -> IpPair { self.gateway_client.private_ips } @@ -182,29 +209,39 @@ impl TopUpMessage for v4::topup::TopUpMessage { } } +impl TopUpMessage for v5::topup::TopUpMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } + + fn credential(&self) -> CredentialSpendingData { + self.credential.clone() + } +} + pub enum AuthenticatorRequest { Initial { msg: Box, protocol: Protocol, - reply_to: Recipient, + reply_to: Option, request_id: u64, }, Final { msg: Box, protocol: Protocol, - reply_to: Recipient, + reply_to: Option, request_id: u64, }, QueryBandwidth { msg: Box, protocol: Protocol, - reply_to: Recipient, + reply_to: Option, request_id: u64, }, TopUpBandwidth { msg: Box, protocol: Protocol, - reply_to: Recipient, + reply_to: Option, request_id: u64, }, } @@ -218,7 +255,7 @@ impl From for AuthenticatorRequest { version: value.version, service_provider_type: ServiceProviderType::Authenticator, }, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, }, v1::request::AuthenticatorRequestData::Final(gateway_client) => Self::Final { @@ -227,7 +264,7 @@ impl From for AuthenticatorRequest { version: value.version, service_provider_type: ServiceProviderType::Authenticator, }, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, }, v1::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { @@ -237,7 +274,7 @@ impl From for AuthenticatorRequest { version: value.version, service_provider_type: ServiceProviderType::Authenticator, }, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, } } @@ -251,20 +288,20 @@ impl From for AuthenticatorRequest { v2::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { msg: Box::new(init_message), protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, }, v2::request::AuthenticatorRequestData::Final(final_message) => Self::Final { msg: final_message, protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, }, v2::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { Self::QueryBandwidth { msg: Box::new(peer_public_key), protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, } } @@ -278,20 +315,20 @@ impl From for AuthenticatorRequest { v3::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { msg: Box::new(init_message), protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, }, v3::request::AuthenticatorRequestData::Final(final_message) => Self::Final { msg: final_message, protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, }, v3::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { Self::QueryBandwidth { msg: Box::new(peer_public_key), protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, } } @@ -299,7 +336,7 @@ impl From for AuthenticatorRequest { Self::TopUpBandwidth { msg: top_up_message, protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, } } @@ -313,20 +350,20 @@ impl From for AuthenticatorRequest { v4::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { msg: Box::new(init_message), protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, }, v4::request::AuthenticatorRequestData::Final(final_message) => Self::Final { msg: final_message, protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, }, v4::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { Self::QueryBandwidth { msg: Box::new(peer_public_key), protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, } } @@ -334,7 +371,42 @@ impl From for AuthenticatorRequest { Self::TopUpBandwidth { msg: top_up_message, protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), + request_id: value.request_id, + } + } + } + } +} + +impl From for AuthenticatorRequest { + fn from(value: v5::request::AuthenticatorRequest) -> Self { + match value.data { + v5::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { + msg: Box::new(init_message), + protocol: value.protocol, + reply_to: None, + request_id: value.request_id, + }, + v5::request::AuthenticatorRequestData::Final(final_message) => Self::Final { + msg: final_message, + protocol: value.protocol, + reply_to: None, + request_id: value.request_id, + }, + v5::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { + Self::QueryBandwidth { + msg: Box::new(peer_public_key), + protocol: value.protocol, + reply_to: None, + request_id: value.request_id, + } + } + v5::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { + Self::TopUpBandwidth { + msg: top_up_message, + protocol: value.protocol, + reply_to: None, request_id: value.request_id, } } diff --git a/common/authenticator-requests/src/v5/conversion.rs b/common/authenticator-requests/src/v5/conversion.rs new file mode 100644 index 0000000000..e3f7da0830 --- /dev/null +++ b/common/authenticator-requests/src/v5/conversion.rs @@ -0,0 +1,478 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; + +use crate::{v4, v5}; + +impl From for v5::request::AuthenticatorRequest { + fn from(authenticator_request: v4::request::AuthenticatorRequest) -> Self { + Self { + protocol: Protocol { + version: 5, + service_provider_type: ServiceProviderType::Authenticator, + }, + data: authenticator_request.data.into(), + request_id: authenticator_request.request_id, + } + } +} + +impl From for v5::request::AuthenticatorRequestData { + fn from(authenticator_request_data: v4::request::AuthenticatorRequestData) -> Self { + match authenticator_request_data { + v4::request::AuthenticatorRequestData::Initial(init_msg) => { + v5::request::AuthenticatorRequestData::Initial(init_msg.into()) + } + v4::request::AuthenticatorRequestData::Final(final_msg) => { + v5::request::AuthenticatorRequestData::Final(Box::new((*final_msg).into())) + } + v4::request::AuthenticatorRequestData::QueryBandwidth(pub_key) => { + v5::request::AuthenticatorRequestData::QueryBandwidth(pub_key) + } + v4::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { + v5::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message.into()) + } + } + } +} + +impl From for v5::registration::InitMessage { + fn from(init_msg: v4::registration::InitMessage) -> Self { + Self { + pub_key: init_msg.pub_key, + } + } +} + +impl From for v5::registration::FinalMessage { + fn from(final_msg: v4::registration::FinalMessage) -> Self { + Self { + gateway_client: final_msg.gateway_client.into(), + credential: final_msg.credential, + } + } +} + +impl From for v5::registration::GatewayClient { + fn from(gateway_client: v4::registration::GatewayClient) -> Self { + Self { + pub_key: gateway_client.pub_key, + private_ips: gateway_client.private_ips.into(), + mac: gateway_client.mac.into(), + } + } +} + +impl From for v4::registration::GatewayClient { + fn from(gateway_client: v5::registration::GatewayClient) -> Self { + Self { + pub_key: gateway_client.pub_key, + private_ips: gateway_client.private_ips.into(), + mac: gateway_client.mac.into(), + } + } +} + +impl From for v5::registration::ClientMac { + fn from(client_mac: v4::registration::ClientMac) -> Self { + Self::new((*client_mac).clone()) + } +} + +impl From for v4::registration::ClientMac { + fn from(client_mac: v5::registration::ClientMac) -> Self { + Self::new((*client_mac).clone()) + } +} + +impl From> for Box { + fn from(top_up_message: Box) -> Self { + Box::new(v5::topup::TopUpMessage { + pub_key: top_up_message.pub_key, + credential: top_up_message.credential, + }) + } +} + +impl From for v5::response::AuthenticatorResponse { + fn from(value: v4::response::AuthenticatorResponse) -> Self { + Self { + protocol: Protocol { + version: 5, + service_provider_type: value.protocol.service_provider_type, + }, + data: value.data.into(), + } + } +} + +impl From for v5::response::AuthenticatorResponseData { + fn from(authenticator_response_data: v4::response::AuthenticatorResponseData) -> Self { + match authenticator_response_data { + v4::response::AuthenticatorResponseData::PendingRegistration(pending_response) => { + v5::response::AuthenticatorResponseData::PendingRegistration( + pending_response.into(), + ) + } + v4::response::AuthenticatorResponseData::Registered(registered_response) => { + v5::response::AuthenticatorResponseData::Registered(registered_response.into()) + } + v4::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => v5::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response.into(), + ), + v4::response::AuthenticatorResponseData::TopUpBandwidth(top_up_response) => { + v5::response::AuthenticatorResponseData::TopUpBandwidth(top_up_response.into()) + } + } + } +} + +impl From for v5::response::RegisteredResponse { + fn from(value: v4::response::RegisteredResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.into(), + } + } +} + +impl From for v5::response::PendingRegistrationResponse { + fn from(value: v4::response::PendingRegistrationResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.into(), + } + } +} + +impl From for v5::registration::RegistrationData { + fn from(value: v4::registration::RegistrationData) -> Self { + Self { + nonce: value.nonce, + gateway_data: value.gateway_data.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v4::registration::RegistrationData { + fn from(value: v5::registration::RegistrationData) -> Self { + Self { + nonce: value.nonce, + gateway_data: value.gateway_data.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v5::response::RemainingBandwidthResponse { + fn from(value: v4::response::RemainingBandwidthResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.map(Into::into), + } + } +} + +impl From for v5::response::TopUpBandwidthResponse { + fn from(value: v4::response::TopUpBandwidthResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.into(), + } + } +} + +impl From for v5::registration::RegistredData { + fn from(value: v4::registration::RegistredData) -> Self { + Self { + pub_key: value.pub_key, + private_ips: value.private_ips.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v5::registration::RemainingBandwidthData { + fn from(value: v4::registration::RemainingBandwidthData) -> Self { + Self { + available_bandwidth: value.available_bandwidth, + } + } +} + +impl From for v5::registration::IpPair { + fn from(value: v4::registration::IpPair) -> Self { + Self { + ipv4: value.ipv4, + ipv6: value.ipv6, + } + } +} + +impl From for v4::registration::IpPair { + fn from(value: v5::registration::IpPair) -> Self { + Self { + ipv4: value.ipv4, + ipv6: value.ipv6, + } + } +} + +#[cfg(test)] +mod tests { + use std::{ + net::{Ipv4Addr, Ipv6Addr}, + str::FromStr, + }; + + use nym_credentials_interface::CredentialSpendingData; + use nym_crypto::asymmetric::encryption::PrivateKey; + use nym_sphinx::addressing::Recipient; + use nym_wireguard_types::PeerPublicKey; + use x25519_dalek::PublicKey; + + use super::*; + use crate::{ + util::tests::{CREDENTIAL_BYTES, RECIPIENT}, + v4, + }; + + #[test] + fn upgrade_initial_req() { + let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); + let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap(); + + let (msg, _) = v4::request::AuthenticatorRequest::new_initial_request( + v4::registration::InitMessage::new(pub_key), + reply_to, + ); + let upgraded_msg = v5::request::AuthenticatorRequest::from(msg); + + assert_eq!( + upgraded_msg.protocol, + Protocol { + version: 5, + service_provider_type: ServiceProviderType::Authenticator + } + ); + assert_eq!( + upgraded_msg.data, + v5::request::AuthenticatorRequestData::Initial(v5::registration::InitMessage { + pub_key + }) + ); + } + + #[test] + fn upgrade_final_req() { + let mut rng = rand::thread_rng(); + + let local_secret = PrivateKey::new(&mut rng); + let remote_secret = x25519_dalek::StaticSecret::random_from_rng(&mut rng); + let ipv4 = Ipv4Addr::from_str("10.10.10.10").unwrap(); + let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap(); + let ips = v4::registration::IpPair::new(ipv4, ipv6); + let nonce = 42; + let gateway_client = v4::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + ips, + nonce, + ); + let credential = Some(CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap()); + let final_message = v4::registration::FinalMessage { + gateway_client: gateway_client.clone(), + credential: credential.clone(), + }; + let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap(); + + let (msg, _) = + v4::request::AuthenticatorRequest::new_final_request(final_message, reply_to); + let upgraded_msg = v5::request::AuthenticatorRequest::from(msg); + + assert_eq!( + upgraded_msg.protocol, + Protocol { + version: 5, + service_provider_type: ServiceProviderType::Authenticator + } + ); + assert_eq!( + upgraded_msg.data, + v5::request::AuthenticatorRequestData::Final(Box::new( + v5::registration::FinalMessage { + gateway_client: v5::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + v5::registration::IpPair::new(ipv4, ipv6), + nonce + ), + credential + } + )) + ); + } + + #[test] + fn upgrade_query_req() { + let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); + let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap(); + + let (msg, _) = v4::request::AuthenticatorRequest::new_query_request(pub_key, reply_to); + let upgraded_msg = v5::request::AuthenticatorRequest::from(msg); + + assert_eq!( + upgraded_msg.protocol, + Protocol { + version: 5, + service_provider_type: ServiceProviderType::Authenticator + } + ); + assert_eq!( + upgraded_msg.data, + v5::request::AuthenticatorRequestData::QueryBandwidth(pub_key) + ); + } + + #[test] + fn upgrade_pending_reg_resp() { + let mut rng = rand::thread_rng(); + + let local_secret = PrivateKey::new(&mut rng); + let remote_secret = x25519_dalek::StaticSecret::random_from_rng(&mut rng); + let ipv4 = Ipv4Addr::from_str("10.10.10.10").unwrap(); + let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap(); + let ips = v4::registration::IpPair::new(ipv4, ipv6); + let nonce = 42; + let wg_port = 51822; + let gateway_data = v4::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + ips, + nonce, + ); + let registration_data = v4::registration::RegistrationData { + nonce, + gateway_data, + wg_port, + }; + let request_id = 123; + let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap(); + + let msg = v4::response::AuthenticatorResponse::new_pending_registration_success( + registration_data, + request_id, + reply_to, + ); + let upgraded_msg = v5::response::AuthenticatorResponse::from(msg); + + assert_eq!( + upgraded_msg.protocol, + Protocol { + version: 5, + service_provider_type: ServiceProviderType::Authenticator + } + ); + + assert_eq!( + upgraded_msg.data, + v5::response::AuthenticatorResponseData::PendingRegistration( + v5::response::PendingRegistrationResponse { + request_id, + reply: v5::registration::RegistrationData { + nonce, + gateway_data: v5::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + v5::registration::IpPair::new(ipv4, ipv6), + nonce + ), + wg_port + } + } + ) + ); + } + + #[test] + fn upgrade_registered_resp() { + let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); + let ipv4 = Ipv4Addr::from_str("10.1.10.10").unwrap(); + let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap(); + let private_ips = v4::registration::IpPair::new(ipv4, ipv6); + let wg_port = 51822; + let registred_data = v4::registration::RegistredData { + pub_key, + private_ips, + wg_port, + }; + let request_id = 123; + let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap(); + + let msg = v4::response::AuthenticatorResponse::new_registered( + registred_data, + reply_to, + request_id, + ); + let upgraded_msg = v5::response::AuthenticatorResponse::from(msg); + + assert_eq!( + upgraded_msg.protocol, + Protocol { + version: 5, + service_provider_type: ServiceProviderType::Authenticator + } + ); + assert_eq!( + upgraded_msg.data, + v5::response::AuthenticatorResponseData::Registered(v5::response::RegisteredResponse { + request_id, + reply: v5::registration::RegistredData { + wg_port, + pub_key, + private_ips: v5::registration::IpPair::new(ipv4, ipv6) + } + }) + ); + } + + #[test] + fn upgrade_remaining_bandwidth_resp() { + let available_bandwidth = 42; + let remaining_bandwidth_data = Some(v4::registration::RemainingBandwidthData { + available_bandwidth, + }); + let request_id = 123; + let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap(); + + let msg = v4::response::AuthenticatorResponse::new_remaining_bandwidth( + remaining_bandwidth_data, + reply_to, + request_id, + ); + let upgraded_msg = v5::response::AuthenticatorResponse::from(msg); + + assert_eq!( + upgraded_msg.protocol, + Protocol { + version: 5, + service_provider_type: ServiceProviderType::Authenticator + } + ); + assert_eq!( + upgraded_msg.data, + v5::response::AuthenticatorResponseData::RemainingBandwidth( + v5::response::RemainingBandwidthResponse { + request_id, + reply: Some(v5::registration::RemainingBandwidthData { + available_bandwidth, + }) + } + ) + ); + } +} diff --git a/common/authenticator-requests/src/v5/mod.rs b/common/authenticator-requests/src/v5/mod.rs new file mode 100644 index 0000000000..2f0df82353 --- /dev/null +++ b/common/authenticator-requests/src/v5/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod conversion; +pub mod registration; +pub mod request; +pub mod response; +pub mod topup; + +pub const VERSION: u8 = 5; diff --git a/common/authenticator-requests/src/v5/registration.rs b/common/authenticator-requests/src/v5/registration.rs new file mode 100644 index 0000000000..9bd5c49e44 --- /dev/null +++ b/common/authenticator-requests/src/v5/registration.rs @@ -0,0 +1,287 @@ +// -2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::Error; +use base64::{engine::general_purpose, Engine}; +use nym_credentials_interface::CredentialSpendingData; +use nym_network_defaults::constants::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6}; +use nym_wireguard_types::PeerPublicKey; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::time::SystemTime; +use std::{fmt, ops::Deref, str::FromStr}; + +#[cfg(feature = "verify")] +use hmac::{Hmac, Mac}; +#[cfg(feature = "verify")] +use nym_crypto::asymmetric::encryption::PrivateKey; +#[cfg(feature = "verify")] +use sha2::Sha256; + +pub type PendingRegistrations = HashMap; +pub type PrivateIPs = HashMap; + +#[cfg(feature = "verify")] +pub type HmacSha256 = Hmac; + +pub type Nonce = u64; +pub type Taken = Option; + +pub const BANDWIDTH_CAP_PER_DAY: u64 = 250 * 1024 * 1024 * 1024; // 250 GB + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct IpPair { + pub ipv4: Ipv4Addr, + pub ipv6: Ipv6Addr, +} + +impl IpPair { + pub fn new(ipv4: Ipv4Addr, ipv6: Ipv6Addr) -> Self { + IpPair { ipv4, ipv6 } + } +} + +impl From<(Ipv4Addr, Ipv6Addr)> for IpPair { + fn from((ipv4, ipv6): (Ipv4Addr, Ipv6Addr)) -> Self { + IpPair { ipv4, ipv6 } + } +} + +impl fmt::Display for IpPair { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "({}, {})", self.ipv4, self.ipv6) + } +} + +impl From for IpPair { + fn from(value: IpAddr) -> Self { + let (before_last_byte, last_byte) = match value { + std::net::IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]), + std::net::IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]), + }; + let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16; + let ipv4 = Ipv4Addr::new( + WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0], + WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1], + before_last_byte, + last_byte, + ); + let ipv6 = Ipv6Addr::new( + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6], + last_bytes, + ); + IpPair::new(ipv4, ipv6) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InitMessage { + /// Base64 encoded x25519 public key + pub pub_key: PeerPublicKey, +} + +impl InitMessage { + pub fn new(pub_key: PeerPublicKey) -> Self { + InitMessage { pub_key } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct FinalMessage { + /// Gateway client data + pub gateway_client: GatewayClient, + + /// Ecash credential + pub credential: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RegistrationData { + pub nonce: u64, + pub gateway_data: GatewayClient, + pub wg_port: u16, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RegistredData { + pub pub_key: PeerPublicKey, + pub private_ips: IpPair, + pub wg_port: u16, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RemainingBandwidthData { + pub available_bandwidth: i64, +} + +/// Client that wants to register sends its PublicKey bytes mac digest encrypted with a DH shared secret. +/// Gateway/Nym node can then verify pub_key payload using the same process +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct GatewayClient { + /// Base64 encoded x25519 public key + pub pub_key: PeerPublicKey, + + /// Assigned private IPs (v4 and v6) + pub private_ips: IpPair, + + /// Sha256 hmac on the data (alongside the prior nonce) + pub mac: ClientMac, +} + +impl GatewayClient { + #[cfg(feature = "verify")] + pub fn new( + local_secret: &PrivateKey, + remote_public: x25519_dalek::PublicKey, + private_ips: IpPair, + nonce: u64, + ) -> Self { + // convert from 1.0 x25519-dalek private key into 2.0 x25519-dalek + #[allow(clippy::expect_used)] + let static_secret = x25519_dalek::StaticSecret::from(local_secret.to_bytes()); + let local_public: x25519_dalek::PublicKey = (&static_secret).into(); + + let dh = static_secret.diffie_hellman(&remote_public); + + // TODO: change that to use our nym_crypto::hmac module instead + #[allow(clippy::expect_used)] + let mut mac = HmacSha256::new_from_slice(dh.as_bytes()) + .expect("x25519 shared secret is always 32 bytes long"); + + mac.update(local_public.as_bytes()); + mac.update(private_ips.to_string().as_bytes()); + mac.update(&nonce.to_le_bytes()); + + GatewayClient { + pub_key: PeerPublicKey::new(local_public), + private_ips, + mac: ClientMac(mac.finalize().into_bytes().to_vec()), + } + } + + // Reusable secret should be gateways Wireguard PK + // Client should perform this step when generating its payload, using its own WG PK + #[cfg(feature = "verify")] + pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + // convert from 1.0 x25519-dalek private key into 2.0 x25519-dalek + #[allow(clippy::expect_used)] + let static_secret = x25519_dalek::StaticSecret::from(gateway_key.to_bytes()); + + let dh = static_secret.diffie_hellman(&self.pub_key); + + // TODO: change that to use our nym_crypto::hmac module instead + #[allow(clippy::expect_used)] + let mut mac = HmacSha256::new_from_slice(dh.as_bytes()) + .expect("x25519 shared secret is always 32 bytes long"); + + mac.update(self.pub_key.as_bytes()); + mac.update(self.private_ips.to_string().as_bytes()); + mac.update(&nonce.to_le_bytes()); + + mac.verify_slice(&self.mac) + .map_err(|source| Error::FailedClientMacVerification { + client: self.pub_key.to_string(), + source, + }) + } + + pub fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +// TODO: change the inner type into generic array of size HmacSha256::OutputSize +// TODO2: rely on our internal crypto/hmac +#[derive(Debug, Clone, PartialEq)] +pub struct ClientMac(Vec); + +impl fmt::Display for ClientMac { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", general_purpose::STANDARD.encode(&self.0)) + } +} + +impl ClientMac { + #[allow(dead_code)] + pub fn new(mac: Vec) -> Self { + ClientMac(mac) + } +} + +impl Deref for ClientMac { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromStr for ClientMac { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mac_bytes: Vec = + general_purpose::STANDARD + .decode(s) + .map_err(|source| Error::MalformedClientMac { + mac: s.to_string(), + source, + })?; + + Ok(ClientMac(mac_bytes)) + } +} + +impl Serialize for ClientMac { + fn serialize(&self, serializer: S) -> Result { + let encoded_key = general_purpose::STANDARD.encode(self.0.clone()); + serializer.serialize_str(&encoded_key) + } +} + +impl<'de> Deserialize<'de> for ClientMac { + fn deserialize>(deserializer: D) -> Result { + let encoded_key = String::deserialize(deserializer)?; + ClientMac::from_str(&encoded_key).map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nym_crypto::asymmetric::encryption; + + #[test] + fn create_ip_pair() { + let ipv4: IpAddr = Ipv4Addr::from_str("10.1.10.50").unwrap().into(); + let ipv6: IpAddr = Ipv6Addr::from_str("fc01::0a32").unwrap().into(); + + assert_eq!(IpPair::from(ipv4), IpPair::from(ipv6)); + } + + #[test] + #[cfg(feature = "verify")] + fn client_request_roundtrip() { + let mut rng = rand::thread_rng(); + + let gateway_key_pair = encryption::KeyPair::new(&mut rng); + let client_key_pair = encryption::KeyPair::new(&mut rng); + + let nonce = 1234567890; + + let client = GatewayClient::new( + client_key_pair.private_key(), + x25519_dalek::PublicKey::from(gateway_key_pair.public_key().to_bytes()), + IpPair::new("10.0.0.42".parse().unwrap(), "fc00::42".parse().unwrap()), + nonce, + ); + assert!(client.verify(gateway_key_pair.private_key(), nonce).is_ok()) + } +} diff --git a/common/authenticator-requests/src/v5/request.rs b/common/authenticator-requests/src/v5/request.rs new file mode 100644 index 0000000000..4405ee75e0 --- /dev/null +++ b/common/authenticator-requests/src/v5/request.rs @@ -0,0 +1,132 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::{ + registration::{FinalMessage, InitMessage}, + topup::TopUpMessage, +}; +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; +use nym_wireguard_types::PeerPublicKey; +use serde::{Deserialize, Serialize}; + +use crate::make_bincode_serializer; + +use super::VERSION; + +fn generate_random() -> u64 { + use rand::RngCore; + let mut rng = rand::rngs::OsRng; + rng.next_u64() +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct AuthenticatorRequest { + pub protocol: Protocol, + pub data: AuthenticatorRequestData, + pub request_id: u64, +} + +impl AuthenticatorRequest { + pub fn from_reconstructed_message( + message: &nym_sphinx::receiver::ReconstructedMessage, + ) -> Result { + use bincode::Options; + make_bincode_serializer().deserialize(&message.message) + } + + pub fn new_initial_request(init_message: InitMessage) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorRequestData::Initial(init_message), + request_id, + }, + request_id, + ) + } + + pub fn new_final_request(final_message: FinalMessage) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorRequestData::Final(Box::new(final_message)), + request_id, + }, + request_id, + ) + } + + pub fn new_query_request(peer_public_key: PeerPublicKey) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorRequestData::QueryBandwidth(peer_public_key), + request_id, + }, + request_id, + ) + } + + pub fn new_topup_request(top_up_message: TopUpMessage) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorRequestData::TopUpBandwidth(Box::new(top_up_message)), + request_id, + }, + request_id, + ) + } + + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum AuthenticatorRequestData { + Initial(InitMessage), + Final(Box), + QueryBandwidth(PeerPublicKey), + TopUpBandwidth(Box), +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[test] + fn check_first_bytes_protocol() { + let version = 5; + let data = AuthenticatorRequest { + protocol: Protocol { + version, + service_provider_type: ServiceProviderType::Authenticator, + }, + data: AuthenticatorRequestData::Initial(InitMessage::new( + PeerPublicKey::from_str("yvNUDpT5l7W/xDhiu6HkqTHDQwbs/B3J5UrLmORl1EQ=").unwrap(), + )), + request_id: 1, + }; + let bytes = *data.to_bytes().unwrap().first_chunk::<2>().unwrap(); + assert_eq!(bytes, [version, ServiceProviderType::Authenticator as u8]); + } +} diff --git a/common/authenticator-requests/src/v5/response.rs b/common/authenticator-requests/src/v5/response.rs new file mode 100644 index 0000000000..044b803d0d --- /dev/null +++ b/common/authenticator-requests/src/v5/response.rs @@ -0,0 +1,132 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData}; +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; +use serde::{Deserialize, Serialize}; + +use crate::make_bincode_serializer; + +use super::VERSION; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct AuthenticatorResponse { + pub protocol: Protocol, + pub data: AuthenticatorResponseData, +} + +impl AuthenticatorResponse { + pub fn new_pending_registration_success( + registration_data: RegistrationData, + request_id: u64, + ) -> Self { + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorResponseData::PendingRegistration(PendingRegistrationResponse { + reply: registration_data, + request_id, + }), + } + } + + pub fn new_registered(registred_data: RegistredData, request_id: u64) -> Self { + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorResponseData::Registered(RegisteredResponse { + reply: registred_data, + request_id, + }), + } + } + + pub fn new_remaining_bandwidth( + remaining_bandwidth_data: Option, + request_id: u64, + ) -> Self { + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorResponseData::RemainingBandwidth(RemainingBandwidthResponse { + reply: remaining_bandwidth_data, + request_id, + }), + } + } + + pub fn new_topup_bandwidth( + remaining_bandwidth_data: RemainingBandwidthData, + request_id: u64, + ) -> Self { + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorResponseData::TopUpBandwidth(TopUpBandwidthResponse { + reply: remaining_bandwidth_data, + request_id, + }), + } + } + + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } + + pub fn from_reconstructed_message( + message: &nym_sphinx::receiver::ReconstructedMessage, + ) -> Result { + use bincode::Options; + make_bincode_serializer().deserialize(&message.message) + } + + pub fn id(&self) -> Option { + match &self.data { + AuthenticatorResponseData::PendingRegistration(response) => Some(response.request_id), + AuthenticatorResponseData::Registered(response) => Some(response.request_id), + AuthenticatorResponseData::RemainingBandwidth(response) => Some(response.request_id), + AuthenticatorResponseData::TopUpBandwidth(response) => Some(response.request_id), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum AuthenticatorResponseData { + PendingRegistration(PendingRegistrationResponse), + Registered(RegisteredResponse), + RemainingBandwidth(RemainingBandwidthResponse), + TopUpBandwidth(TopUpBandwidthResponse), +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct PendingRegistrationResponse { + pub request_id: u64, + pub reply: RegistrationData, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct RegisteredResponse { + pub request_id: u64, + pub reply: RegistredData, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct RemainingBandwidthResponse { + pub request_id: u64, + pub reply: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct TopUpBandwidthResponse { + pub request_id: u64, + pub reply: RemainingBandwidthData, +} diff --git a/common/authenticator-requests/src/v5/topup.rs b/common/authenticator-requests/src/v5/topup.rs new file mode 100644 index 0000000000..1163d07f12 --- /dev/null +++ b/common/authenticator-requests/src/v5/topup.rs @@ -0,0 +1,15 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::CredentialSpendingData; +use nym_wireguard_types::PeerPublicKey; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct TopUpMessage { + /// Base64 encoded x25519 public key + pub pub_key: PeerPublicKey, + + /// Ecash credential + pub credential: CredentialSpendingData, +} diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index a794ca4e98..028db3681a 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node" -version = "1.6.0" +version = "1.6.1" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -101,4 +101,4 @@ cargo_metadata = { workspace = true } [lints] -workspace = true \ No newline at end of file +workspace = true diff --git a/service-providers/authenticator/src/cli/request.rs b/service-providers/authenticator/src/cli/request.rs index 0a8dfe3cd5..a3f39ddc91 100644 --- a/service-providers/authenticator/src/cli/request.rs +++ b/service-providers/authenticator/src/cli/request.rs @@ -111,16 +111,16 @@ pub(crate) async fn execute(args: &Request) -> Result<(), AuthenticatorError> { let authenticator_recipient = Recipient::from_str(&args.authenticator_recipient)?; let (request, _) = match request_data { AuthenticatorRequestData::Initial(init_message) => { - AuthenticatorRequest::new_initial_request(init_message, *mixnet_client.nym_address()) + AuthenticatorRequest::new_initial_request(init_message) } AuthenticatorRequestData::Final(final_message) => { - AuthenticatorRequest::new_final_request(*final_message, *mixnet_client.nym_address()) + AuthenticatorRequest::new_final_request(*final_message) } AuthenticatorRequestData::QueryBandwidth(query_message) => { - AuthenticatorRequest::new_query_request(query_message, *mixnet_client.nym_address()) + AuthenticatorRequest::new_query_request(query_message) } AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { - AuthenticatorRequest::new_topup_request(*top_up_message, *mixnet_client.nym_address()) + AuthenticatorRequest::new_topup_request(*top_up_message) } }; mixnet_client diff --git a/service-providers/authenticator/src/error.rs b/service-providers/authenticator/src/error.rs index 0430a0805b..59a1597147 100644 --- a/service-providers/authenticator/src/error.rs +++ b/service-providers/authenticator/src/error.rs @@ -89,6 +89,9 @@ pub enum AuthenticatorError { #[error("unknown version number")] UnknownVersion, + #[error("missing reply_to for old client")] + MissingReplyToForOldClient, + #[error("{0}")] PublicKey(#[from] nym_wireguard_types::Error), diff --git a/service-providers/authenticator/src/mixnet_listener.rs b/service-providers/authenticator/src/mixnet_listener.rs index dcd3c8e31e..2ebb39b2c7 100644 --- a/service-providers/authenticator/src/mixnet_listener.rs +++ b/service-providers/authenticator/src/mixnet_listener.rs @@ -21,7 +21,7 @@ use nym_authenticator_requests::{ AuthenticatorRequest, AuthenticatorVersion, FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, }, - v1, v2, v3, v4, CURRENT_VERSION, + v1, v2, v3, v4, v5, CURRENT_VERSION, }; use nym_credential_verification::{ bandwidth_storage_manager::BandwidthStorageManager, ecash::EcashManager, @@ -42,7 +42,7 @@ use rand::{prelude::IteratorRandom, thread_rng}; use tokio::sync::RwLock; use tokio_stream::wrappers::IntervalStream; -type AuthenticatorHandleResult = Result<(Vec, Recipient)>; +type AuthenticatorHandleResult = Result<(Vec, Option)>; const DEFAULT_REGISTRATION_TIMEOUT_CHECK: Duration = Duration::from_secs(60); // 1 minute pub(crate) struct RegistredAndFree { @@ -155,7 +155,7 @@ impl MixnetListener { init_message: Box, protocol: Protocol, request_id: u64, - reply_to: Recipient, + reply_to: Option, ) -> AuthenticatorHandleResult { let remote_public = init_message.pub_key(); let nonce: u64 = fastrand::u64(..); @@ -178,7 +178,7 @@ impl MixnetListener { wg_port: registration_data.wg_port, }, request_id, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() .map_err(|err| { @@ -198,7 +198,7 @@ impl MixnetListener { wg_port: registration_data.wg_port, }, request_id, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() .map_err(|err| { @@ -218,7 +218,7 @@ impl MixnetListener { wg_port: registration_data.wg_port, }, request_id, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() .map_err(|err| { @@ -228,12 +228,26 @@ impl MixnetListener { AuthenticatorVersion::V4 => { v4::response::AuthenticatorResponse::new_pending_registration_success( v4::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data.clone().into(), + wg_port: registration_data.wg_port, + }, + request_id, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V5 => { + v5::response::AuthenticatorResponse::new_pending_registration_success( + v5::registration::RegistrationData { nonce: registration_data.nonce, gateway_data: registration_data.gateway_data.clone(), wg_port: registration_data.wg_port, }, request_id, - reply_to, ) .to_bytes() .map_err(|err| { @@ -272,7 +286,7 @@ impl MixnetListener { private_ip: allowed_ipv4.into(), wg_port: self.config.authenticator.announced_port, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -285,7 +299,7 @@ impl MixnetListener { private_ip: allowed_ipv4.into(), wg_port: self.config.authenticator.announced_port, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -298,7 +312,7 @@ impl MixnetListener { private_ip: allowed_ipv4.into(), wg_port: self.config.authenticator.announced_port, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -311,7 +325,19 @@ impl MixnetListener { private_ips: (allowed_ipv4, allowed_ipv6).into(), wg_port: self.config.authenticator.announced_port, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, + request_id, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })?, + AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_registered( + v5::registration::RegistredData { + pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + private_ips: (allowed_ipv4, allowed_ipv6).into(), + wg_port: self.config.authenticator.announced_port, + }, request_id, ) .to_bytes() @@ -360,7 +386,7 @@ impl MixnetListener { wg_port: registration_data.wg_port, }, request_id, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() .map_err(|err| { @@ -380,7 +406,7 @@ impl MixnetListener { wg_port: registration_data.wg_port, }, request_id, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() .map_err(|err| { @@ -400,7 +426,7 @@ impl MixnetListener { wg_port: registration_data.wg_port, }, request_id, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() .map_err(|err| { @@ -410,12 +436,26 @@ impl MixnetListener { AuthenticatorVersion::V4 => { v4::response::AuthenticatorResponse::new_pending_registration_success( v4::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data.into(), + wg_port: registration_data.wg_port, + }, + request_id, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V5 => { + v5::response::AuthenticatorResponse::new_pending_registration_success( + v5::registration::RegistrationData { nonce: registration_data.nonce, gateway_data: registration_data.gateway_data, wg_port: registration_data.wg_port, }, request_id, - reply_to, ) .to_bytes() .map_err(|err| { @@ -433,7 +473,7 @@ impl MixnetListener { final_message: Box, protocol: Protocol, request_id: u64, - reply_to: Recipient, + reply_to: Option, ) -> AuthenticatorHandleResult { let mut registred_and_free = self.registred_and_free.write().await; let registration_data = registred_and_free @@ -500,7 +540,7 @@ impl MixnetListener { private_ip: registration_data.gateway_data.private_ips.ipv4.into(), wg_port: registration_data.wg_port, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -511,7 +551,7 @@ impl MixnetListener { private_ip: registration_data.gateway_data.private_ips.ipv4.into(), wg_port: registration_data.wg_port, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -522,18 +562,28 @@ impl MixnetListener { private_ip: registration_data.gateway_data.private_ips.ipv4.into(), wg_port: registration_data.wg_port, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::new_registered( v4::registration::RegistredData { + pub_key: registration_data.gateway_data.pub_key, + private_ips: registration_data.gateway_data.private_ips.into(), + wg_port: registration_data.wg_port, + }, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, + request_id, + ) + .to_bytes() + .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_registered( + v5::registration::RegistredData { pub_key: registration_data.gateway_data.pub_key, private_ips: registration_data.gateway_data.private_ips, wg_port: registration_data.wg_port, }, - reply_to, request_id, ) .to_bytes() @@ -579,7 +629,7 @@ impl MixnetListener { msg: Box, protocol: Protocol, request_id: u64, - reply_to: Recipient, + reply_to: Option, ) -> AuthenticatorHandleResult { let bandwidth_data = self.peer_manager.query_bandwidth(msg).await?; let bytes = match AuthenticatorVersion::from(protocol) { @@ -589,7 +639,7 @@ impl MixnetListener { available_bandwidth: data.available_bandwidth as u64, suspended: false, }), - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -602,7 +652,7 @@ impl MixnetListener { bandwidth_data.map(|data| v2::registration::RemainingBandwidthData { available_bandwidth: data.available_bandwidth, }), - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -615,7 +665,7 @@ impl MixnetListener { bandwidth_data.map(|data| v3::registration::RemainingBandwidthData { available_bandwidth: data.available_bandwidth, }), - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -628,7 +678,19 @@ impl MixnetListener { bandwidth_data.map(|data| v4::registration::RemainingBandwidthData { available_bandwidth: data.available_bandwidth, }), - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, + request_id, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V5 => { + v5::response::AuthenticatorResponse::new_remaining_bandwidth( + bandwidth_data.map(|data| v5::registration::RemainingBandwidthData { + available_bandwidth: data.available_bandwidth, + }), request_id, ) .to_bytes() @@ -646,7 +708,7 @@ impl MixnetListener { msg: Box, protocol: Protocol, request_id: u64, - reply_to: Recipient, + reply_to: Option, ) -> AuthenticatorHandleResult { let Some(ecash_verifier) = self.ecash_verifier.clone() else { return Err(AuthenticatorError::UnsupportedOperation); @@ -681,11 +743,19 @@ impl MixnetListener { let available_bandwidth = verifier.verify().await?; let bytes = match AuthenticatorVersion::from(protocol) { + AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_topup_bandwidth( + v5::registration::RemainingBandwidthData { + available_bandwidth, + }, + request_id, + ) + .to_bytes() + .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::new_topup_bandwidth( v4::registration::RemainingBandwidthData { available_bandwidth, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -694,7 +764,7 @@ impl MixnetListener { v3::registration::RemainingBandwidthData { available_bandwidth, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -762,10 +832,10 @@ impl MixnetListener { async fn handle_response( &self, response: Vec, - recipient: Recipient, + recipient: Option, sender_tag: Option, ) -> Result<()> { - let input_message = create_input_message(recipient, sender_tag, response); + let input_message = create_input_message(recipient, sender_tag, response)?; self.mixnet_client .send(input_message) .await @@ -866,17 +936,30 @@ fn deserialize_request(reconstructed: &ReconstructedMessage) -> Result, reply_to_tag: Option, response_packet: Vec, -) -> InputMessage { +) -> Result { let lane = TransmissionLane::General; let packet_type = None; if let Some(reply_to_tag) = reply_to_tag { log::debug!("Creating message using SURB"); - InputMessage::new_reply(reply_to_tag, response_packet, lane, packet_type) - } else { + Ok(InputMessage::new_reply( + reply_to_tag, + response_packet, + lane, + packet_type, + )) + } else if let Some(nym_address) = nym_address { log::debug!("Creating message using nym_address"); - InputMessage::new_regular(nym_address, response_packet, lane, packet_type) + Ok(InputMessage::new_regular( + nym_address, + response_packet, + lane, + packet_type, + )) + } else { + log::error!("No nym-address or sender tag provided"); + Err(AuthenticatorError::MissingReplyToForOldClient) } } From eacaf844301b3a2b8a691d517f2e7190cfd7335b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 5 Mar 2025 16:43:56 +0000 Subject: [PATCH 04/18] add full response body to error message upon decoding failure (#5566) --- common/http-api-client/src/lib.rs | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index 08c704897d..a8099ce111 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -210,6 +210,12 @@ pub enum HttpClientError { #[error("failed to resolve request. status: '{status}', additional error message: {error}")] EndpointFailure { status: StatusCode, error: E }, + #[error("failed to decode response body: {source} from {content}")] + ResponseDecodeFailure { + source: serde_json::Error, + content: String, + }, + #[cfg(target_arch = "wasm32")] #[error("the request has timed out")] RequestTimeout, @@ -866,19 +872,19 @@ where } if res.status().is_success() { - #[cfg(debug_assertions)] - { - let text = res.text().await.inspect_err(|err| { - tracing::error!("Couldn't even get response text: {err}"); - })?; - tracing::trace!("Result:\n{:#?}", text); - - serde_json::from_str(&text) - .map_err(|err| HttpClientError::GenericRequestFailure(err.to_string())) + // internally reqwest is first retrieving bytes and then performing parsing via serde_json + // (and similarly does the same thing for text()) + let full = res.bytes().await?; + match serde_json::from_slice(&full) { + Ok(data) => Ok(data), + Err(err) => { + let text = String::from_utf8_lossy(&full); + Err(HttpClientError::ResponseDecodeFailure { + source: err, + content: text.into_owned(), + }) + } } - - #[cfg(not(debug_assertions))] - Ok(res.json().await?) } else if res.status() == StatusCode::NOT_FOUND { Err(HttpClientError::NotFound) } else { From aa2f3369043925820490d66fde769378a8ff3877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 5 Mar 2025 16:44:35 +0000 Subject: [PATCH 05/18] hotfix: ensure we bail on merkle leaves insertion upon missing data (#5565) * hotfix: ensure we bail on merkle leaves insertion upon missing data * Update Cargo.toml --------- Co-authored-by: benedetta davico <46782255+benedettadavico@users.noreply.github.com> --- nym-api/Cargo.toml | 4 ++-- nym-api/src/ecash/state/local.rs | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index f95baabd00..239b483bf2 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-api" license = "GPL-3.0" -version = "1.1.51" +version = "1.1.52" authors.workspace = true edition = "2021" rust-version.workspace = true @@ -144,4 +144,4 @@ rand_chacha = { workspace = true } sha2 = "0.9" [lints] -workspace = true \ No newline at end of file +workspace = true diff --git a/nym-api/src/ecash/state/local.rs b/nym-api/src/ecash/state/local.rs index 61b5bab2c4..93cc576a85 100644 --- a/nym-api/src/ecash/state/local.rs +++ b/nym-api/src/ecash/state/local.rs @@ -33,14 +33,17 @@ impl DailyMerkleTree { .into_iter() .map(|l| (l.merkle_index, l)) .collect(); + let total_leaves = leaves.len(); let mut sorted_leaves = Vec::new(); for i in 0..leaves.len() { if let Some(next_leaf) = leaves.remove(&i) { sorted_leaves.push(next_leaf); } else { - let lost = leaves.len() - i + 1; - error!("failed to produce consistent merkle tree. there was no leaf with index {i}. at least {lost} leaves got lost") + let lost = total_leaves - i + 1; + error!("failed to produce consistent merkle tree. there was no leaf with index {i}. at least {lost} leaves got lost"); + // we have to drop all data above that height because we can't rebuild the full tree + break; } } From 0d8b3abc6fabfd241c479e7a7bebb3455fd52b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 5 Mar 2025 23:07:32 +0100 Subject: [PATCH 06/18] Deserialize v5 authenticator requests (#5568) --- Cargo.lock | 2 +- .../authenticator/src/mixnet_listener.rs | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 9cce5fff5f..bbaab3e146 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4747,7 +4747,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "nym-api" -version = "1.1.51" +version = "1.1.52" dependencies = [ "anyhow", "async-trait", diff --git a/service-providers/authenticator/src/mixnet_listener.rs b/service-providers/authenticator/src/mixnet_listener.rs index 2ebb39b2c7..d88a1185c5 100644 --- a/service-providers/authenticator/src/mixnet_listener.rs +++ b/service-providers/authenticator/src/mixnet_listener.rs @@ -928,6 +928,17 @@ fn deserialize_request(reconstructed: &ReconstructedMessage) -> Result { + if request_type == ServiceProviderType::Authenticator as u8 { + v5::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) + .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { + source: err, + }) + .map(Into::into) + } else { + Err(AuthenticatorError::InvalidPacketType(request_type)) + } + } [version, _] => { log::info!("Received packet with invalid version: v{version}"); Err(AuthenticatorError::InvalidPacketVersion(version)) From 8ddef08c72b0047dc8a001e2072cfbec88d0ac3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 6 Mar 2025 10:09:15 +0100 Subject: [PATCH 07/18] Tweak surb management to be more conservative (#5570) To reduce the risk of the IPR DoS the client: - Lower the timeout until the IPR will disconnect a client - Reduce fewer surbs at a time. Large surb requests increases the latency until all fragments in the response have been delivered. The efficiency gains of having large surb requests dimishes quickly for large sizes as well --- common/client-core/config-types/src/lib.rs | 2 +- .../src/client/real_messages_control/message_handler.rs | 6 ++++-- .../client-core/src/client/replies/reply_controller/mod.rs | 2 +- service-providers/ip-packet-router/src/constants.rs | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/common/client-core/config-types/src/lib.rs b/common/client-core/config-types/src/lib.rs index 8eb95d62f1..82e93f9b09 100644 --- a/common/client-core/config-types/src/lib.rs +++ b/common/client-core/config-types/src/lib.rs @@ -50,7 +50,7 @@ const DEFAULT_MINIMUM_REPLY_SURB_THRESHOLD_BUFFER: usize = 0; // define how much to request at once // clients/client-core/src/client/replies/reply_controller.rs const DEFAULT_MINIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 10; -const DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 100; +const DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 30; const DEFAULT_MAXIMUM_ALLOWED_SURB_REQUEST_SIZE: u32 = 500; diff --git a/common/client-core/src/client/real_messages_control/message_handler.rs b/common/client-core/src/client/real_messages_control/message_handler.rs index b8417cead9..e0d0884e96 100644 --- a/common/client-core/src/client/real_messages_control/message_handler.rs +++ b/common/client-core/src/client/real_messages_control/message_handler.rs @@ -33,10 +33,12 @@ pub enum PreparationError { #[error(transparent)] NymTopologyError(#[from] NymTopologyError), - #[error("The received message cannot be sent using a single reply surb. It ended up getting split into {fragments} fragments.")] + #[error("message too long for a single SURB, splitting into {fragments} fragments.")] MessageTooLongForSingleSurb { fragments: usize }, - #[error("Not enough reply SURBs to send the message. We have {available} available and require at least {required}.")] + #[error( + "not enough reply SURBs to send the message, available: {available} required: {required}." + )] NotEnoughSurbs { available: usize, required: usize }, } diff --git a/common/client-core/src/client/replies/reply_controller/mod.rs b/common/client-core/src/client/replies/reply_controller/mod.rs index fb8cdc468b..7b59359407 100644 --- a/common/client-core/src/client/replies/reply_controller/mod.rs +++ b/common/client-core/src/client/replies/reply_controller/mod.rs @@ -746,7 +746,7 @@ where .request_additional_reply_surbs(target, request_size) .await { - warn!("failed to request additional surbs... - {err}") + info!("{err}") } } diff --git a/service-providers/ip-packet-router/src/constants.rs b/service-providers/ip-packet-router/src/constants.rs index 128afa271b..a707199977 100644 --- a/service-providers/ip-packet-router/src/constants.rs +++ b/service-providers/ip-packet-router/src/constants.rs @@ -7,8 +7,8 @@ use std::time::Duration; pub(crate) const DISCONNECT_TIMER_INTERVAL: Duration = Duration::from_secs(10); // We consider a client inactive if it hasn't sent any mixnet packets in this duration -pub(crate) const CLIENT_MIXNET_INACTIVITY_TIMEOUT: Duration = Duration::from_secs(5 * 60); +pub(crate) const CLIENT_MIXNET_INACTIVITY_TIMEOUT: Duration = Duration::from_secs(60); // We consider a client handler inactive if it hasn't received any packets from the tun device in // this duration -pub(crate) const CLIENT_HANDLER_ACTIVITY_TIMEOUT: Duration = Duration::from_secs(10 * 60); +pub(crate) const CLIENT_HANDLER_ACTIVITY_TIMEOUT: Duration = Duration::from_secs(5 * 60); From 698883c03fa559247b73185c6b56ff46ce5a0be6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 6 Mar 2025 09:18:39 +0000 Subject: [PATCH 08/18] feature: v2 authentication request (#5537) (#5563) * introduced v2 authentication request between clients and gateways * client to send v2 auth when possible * added persistence to last used authentication timestamp * added clients identity to signed plaintext --- Cargo.lock | 3 + .../gateway-client/src/client/mod.rs | 92 ++++-- common/gateway-requests/Cargo.toml | 3 + .../src/authentication/encrypted_address.rs | 6 + common/gateway-requests/src/lib.rs | 21 +- common/gateway-requests/src/types/error.rs | 28 ++ .../src/types/text_request/authenticate.rs | 142 +++++++++ .../{text_request.rs => text_request/mod.rs} | 23 +- ...303120000_add_authentication_timestamp.sql | 7 + common/gateway-storage/src/lib.rs | 14 + common/gateway-storage/src/models.rs | 2 +- common/gateway-storage/src/shared_keys.rs | 28 +- gateway/src/config.rs | 3 + .../node/client_handling/active_clients.rs | 33 +- .../client_handling/websocket/common_state.rs | 12 +- .../connection_handler/authenticated.rs | 4 +- .../websocket/connection_handler/fresh.rs | 289 ++++++++++++------ .../websocket/connection_handler/helpers.rs | 37 +++ .../websocket/connection_handler/mod.rs | 10 +- .../src/node/client_handling/websocket/mod.rs | 2 +- gateway/src/node/mod.rs | 7 +- nym-node/src/config/gateway_tasks.rs | 9 +- nym-node/src/config/helpers.rs | 1 + 23 files changed, 624 insertions(+), 152 deletions(-) create mode 100644 common/gateway-requests/src/types/text_request/authenticate.rs rename common/gateway-requests/src/types/{text_request.rs => text_request/mod.rs} (89%) create mode 100644 common/gateway-storage/migrations/20250303120000_add_authentication_timestamp.sql create mode 100644 gateway/src/node/client_handling/websocket/connection_handler/helpers.rs diff --git a/Cargo.lock b/Cargo.lock index bbaab3e146..5f75819cd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5760,13 +5760,16 @@ dependencies = [ "nym-credentials-interface", "nym-crypto", "nym-pemstore", + "nym-serde-helpers", "nym-sphinx", "nym-task", "rand 0.8.5", "serde", "serde_json", "strum 0.26.3", + "subtle 2.6.1", "thiserror 2.0.11", + "time", "tokio", "tracing", "tungstenite 0.20.1", diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index 84cb571c72..6bff2c1805 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -20,8 +20,8 @@ use nym_credentials_interface::TicketType; use nym_crypto::asymmetric::identity; use nym_gateway_requests::registration::handshake::client_handshake; use nym_gateway_requests::{ - BinaryRequest, ClientControlRequest, ClientRequest, SensitiveServerResponse, ServerResponse, - SharedGatewayKey, SharedSymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION, + BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersionExt, + SensitiveServerResponse, ServerResponse, SharedGatewayKey, SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION, }; use nym_sphinx::forwarding::packet::MixPacket; @@ -563,28 +563,10 @@ impl GatewayClient { Ok(zeroizing_updated_key) } - async fn authenticate(&mut self) -> Result<(), GatewayClientError> { - let Some(shared_key) = self.shared_key.as_ref() else { - return Err(GatewayClientError::NoSharedKeyAvailable); - }; - - if !self.connection.is_established() { - return Err(GatewayClientError::ConnectionNotEstablished); - } - debug!("authenticating with gateway"); - - let self_address = self - .local_identity - .as_ref() - .public_key() - .derive_destination_address(); - - let msg = ClientControlRequest::new_authenticate( - self_address, - shared_key, - self.cfg.bandwidth.require_tickets, - )?; - + async fn send_authenticate_request_and_handle_response( + &mut self, + msg: ClientControlRequest, + ) -> Result<(), GatewayClientError> { match self.send_websocket_message(msg).await? { ServerResponse::Authenticate { protocol_version, @@ -608,6 +590,51 @@ impl GatewayClient { } } + async fn authenticate_v1(&mut self) -> Result<(), GatewayClientError> { + debug!("using v1 authentication"); + + let Some(shared_key) = self.shared_key.as_ref() else { + return Err(GatewayClientError::NoSharedKeyAvailable); + }; + + let self_address = self + .local_identity + .public_key() + .derive_destination_address(); + + let msg = ClientControlRequest::new_authenticate( + self_address, + shared_key, + self.cfg.bandwidth.require_tickets, + )?; + self.send_authenticate_request_and_handle_response(msg) + .await + } + + async fn authenticate_v2(&mut self) -> Result<(), GatewayClientError> { + debug!("using v2 authentication"); + let Some(shared_key) = self.shared_key.as_ref() else { + return Err(GatewayClientError::NoSharedKeyAvailable); + }; + + let msg = ClientControlRequest::new_authenticate_v2(shared_key, &self.local_identity)?; + self.send_authenticate_request_and_handle_response(msg) + .await + } + + async fn authenticate(&mut self, use_v2: bool) -> Result<(), GatewayClientError> { + if !self.connection.is_established() { + return Err(GatewayClientError::ConnectionNotEstablished); + } + debug!("authenticating with gateway"); + + if use_v2 { + self.authenticate_v2().await + } else { + self.authenticate_v1().await + } + } + /// Helper method to either call register or authenticate based on self.shared_key value #[instrument(skip_all, fields( @@ -623,19 +650,25 @@ impl GatewayClient { } // 1. check gateway's protocol version - let supports_aes_gcm_siv = match self.get_gateway_protocol().await { - Ok(protocol) => protocol >= AES_GCM_SIV_PROTOCOL_VERSION, + let gw_protocol = match self.get_gateway_protocol().await { + Ok(protocol) => Some(protocol), Err(_) => { // if we failed to send the request, it means the gateway is running the old binary, // so it has reset our connection - we have to reconnect self.establish_connection().await?; - false + None } }; + let supports_aes_gcm_siv = gw_protocol.supports_aes256_gcm_siv(); + let supports_auth_v2 = gw_protocol.supports_authenticate_v2(); + if !supports_aes_gcm_siv { warn!("this gateway is on an old version that doesn't support AES256-GCM-SIV"); } + if !supports_auth_v2 { + warn!("this gateway is on an old version that doesn't support authentication v2") + } if self.authenticated { debug!("Already authenticated"); @@ -650,7 +683,7 @@ impl GatewayClient { } if self.shared_key.is_some() { - self.authenticate().await?; + self.authenticate(supports_auth_v2).await?; if self.authenticated { // if we are authenticated it means we MUST have an associated shared_key @@ -983,7 +1016,8 @@ impl GatewayClient { } // if we're reconnecting, because we lost connection, we need to re-authenticate the connection - self.authenticate().await?; + self.authenticate(self.negotiated_protocol.supports_authenticate_v2()) + .await?; // this call is NON-blocking self.start_listening_for_mixnet_messages()?; diff --git a/common/gateway-requests/Cargo.toml b/common/gateway-requests/Cargo.toml index b461c428c1..3448184eae 100644 --- a/common/gateway-requests/Cargo.toml +++ b/common/gateway-requests/Cargo.toml @@ -20,11 +20,14 @@ serde_json = { workspace = true } strum = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true, features = ["log"] } +time = { workspace = true } +subtle = { workspace = true } zeroize = { workspace = true } nym-crypto = { path = "../crypto", features = ["aead", "hashing"] } nym-pemstore = { path = "../pemstore" } nym-sphinx = { path = "../nymsphinx" } +nym-serde-helpers = { path = "../serde-helpers", features = ["base64"] } nym-task = { path = "../task" } nym-credentials = { path = "../credentials" } diff --git a/common/gateway-requests/src/authentication/encrypted_address.rs b/common/gateway-requests/src/authentication/encrypted_address.rs index 8b81074454..772b8ca91b 100644 --- a/common/gateway-requests/src/authentication/encrypted_address.rs +++ b/common/gateway-requests/src/authentication/encrypted_address.rs @@ -15,6 +15,12 @@ use thiserror::Error; // this is no longer constant size due to the differences in ciphertext between aes128ctr and aes256gcm-siv (inclusion of tag) pub struct EncryptedAddressBytes(Vec); +impl From> for EncryptedAddressBytes { + fn from(encrypted_address: Vec) -> Self { + EncryptedAddressBytes(encrypted_address) + } +} + #[derive(Debug, Error)] pub enum EncryptedAddressConversionError { #[error("Failed to decode the encrypted address - {0}")] diff --git a/common/gateway-requests/src/lib.rs b/common/gateway-requests/src/lib.rs index b7af197cc5..0d300bc4a4 100644 --- a/common/gateway-requests/src/lib.rs +++ b/common/gateway-requests/src/lib.rs @@ -19,7 +19,7 @@ pub use shared_key::{ SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey, }; -pub const CURRENT_PROTOCOL_VERSION: u8 = AES_GCM_SIV_PROTOCOL_VERSION; +pub const CURRENT_PROTOCOL_VERSION: u8 = AUTHENTICATE_V2_PROTOCOL_VERSION; /// Defines the current version of the communication protocol between gateway and clients. /// It has to be incremented for any breaking change. @@ -27,10 +27,29 @@ pub const CURRENT_PROTOCOL_VERSION: u8 = AES_GCM_SIV_PROTOCOL_VERSION; // 1 - initial release // 2 - changes to client credentials structure // 3 - change to AES-GCM-SIV and non-zero IVs +// 4 - introduction of v2 authentication protocol to prevent reply attacks pub const INITIAL_PROTOCOL_VERSION: u8 = 1; pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: u8 = 2; pub const AES_GCM_SIV_PROTOCOL_VERSION: u8 = 3; +pub const AUTHENTICATE_V2_PROTOCOL_VERSION: u8 = 4; // TODO: could using `Mac` trait here for OutputSize backfire? // Should hmac itself be exposed, imported and used instead? pub type LegacyGatewayMacSize = ::OutputSize; + +pub trait GatewayProtocolVersionExt { + fn supports_aes256_gcm_siv(&self) -> bool; + fn supports_authenticate_v2(&self) -> bool; +} + +impl GatewayProtocolVersionExt for Option { + fn supports_aes256_gcm_siv(&self) -> bool { + let Some(protocol) = *self else { return false }; + protocol >= AES_GCM_SIV_PROTOCOL_VERSION + } + + fn supports_authenticate_v2(&self) -> bool { + let Some(protocol) = *self else { return false }; + protocol >= AUTHENTICATE_V2_PROTOCOL_VERSION + } +} diff --git a/common/gateway-requests/src/types/error.rs b/common/gateway-requests/src/types/error.rs index 31d94b75b7..0a785e60ec 100644 --- a/common/gateway-requests/src/types/error.rs +++ b/common/gateway-requests/src/types/error.rs @@ -3,6 +3,7 @@ use crate::SharedKeyUsageError; use nym_credentials_interface::CompactEcashError; +use nym_crypto::asymmetric::ed25519::SignatureError; use nym_sphinx::addressing::nodes::NymNodeRoutingAddressError; use nym_sphinx::forwarding::packet::MixPacketFormattingError; use nym_sphinx::params::packet_sizes::PacketSize; @@ -92,7 +93,34 @@ pub enum GatewayRequestsError { #[error("the provided [v1] credential has invalid number of parameters - {0}")] InvalidNumberOfEmbededParameters(u32), + #[error("failed to authenticate the client: {0}")] + Authentication(#[from] AuthenticationFailure), + // variant to catch legacy errors #[error("{0}")] Other(String), } + +#[derive(Debug, Error)] +pub enum AuthenticationFailure { + #[error(transparent)] + KeyUsageFailure(#[from] SharedKeyUsageError), + + #[error("failed to verify provided address ciphertext")] + MalformedCiphertext, + + #[error("failed to verify request signature")] + InvalidSignature(#[from] SignatureError), + + #[error("provided request timestamp is in the future")] + RequestTimestampInFuture, + + #[error("the client is not registered")] + NotRegistered, + + #[error("the provided request is too stale to process")] + StaleRequest, + + #[error("the provided request timestamp is smaller or equal to a one previously used")] + RequestReuse, +} diff --git a/common/gateway-requests/src/types/text_request/authenticate.rs b/common/gateway-requests/src/types/text_request/authenticate.rs new file mode 100644 index 0000000000..6015beb8ad --- /dev/null +++ b/common/gateway-requests/src/types/text_request/authenticate.rs @@ -0,0 +1,142 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{AuthenticationFailure, GatewayRequestsError, SharedGatewayKey}; +use nym_crypto::asymmetric::ed25519; +use serde::{Deserialize, Serialize}; +use std::iter; +use std::time::Duration; +use subtle::ConstantTimeEq; +use time::OffsetDateTime; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AuthenticateRequest { + #[serde(flatten)] + pub content: AuthenticateRequestContent, + + pub request_signature: ed25519::Signature, +} + +impl AuthenticateRequest { + pub fn new( + protocol_version: u8, + shared_key: &SharedGatewayKey, + identity_keys: &ed25519::KeyPair, + ) -> Result { + let content = AuthenticateRequestContent::new( + protocol_version, + shared_key, + *identity_keys.public_key(), + )?; + let plaintext = content.plaintext(); + let request_signature = identity_keys.private_key().sign(&plaintext); + + Ok(AuthenticateRequest { + content, + request_signature, + }) + } + + pub fn verify_timestamp(&self, max_request_age: Duration) -> Result<(), AuthenticationFailure> { + let now = OffsetDateTime::now_utc(); + if self.content.request_timestamp() + max_request_age < now { + return Err(AuthenticationFailure::StaleRequest); + } + if self.content.request_timestamp() > now { + return Err(AuthenticationFailure::RequestTimestampInFuture); + } + Ok(()) + } + + pub fn ensure_timestamp_not_reused( + &self, + previous: OffsetDateTime, + ) -> Result<(), AuthenticationFailure> { + if self.content.request_timestamp() <= previous { + return Err(AuthenticationFailure::RequestReuse); + } + Ok(()) + } + + pub fn verify_ciphertext( + &self, + shared_key: &SharedGatewayKey, + ) -> Result<(), AuthenticationFailure> { + let expected = shared_key.encrypt( + self.content + .client_identity + .derive_destination_address() + .as_bytes_ref(), + Some(&self.content.nonce), + )?; + + if !bool::from(expected.ct_eq(&self.content.address_ciphertext)) { + return Err(AuthenticationFailure::MalformedCiphertext); + } + Ok(()) + } + + pub fn verify_signature(&self) -> Result<(), AuthenticationFailure> { + let plaintext = self.content.plaintext(); + self.content + .client_identity + .verify(plaintext, &self.request_signature) + .map_err(Into::into) + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AuthenticateRequestContent { + pub protocol_version: u8, + + // this is identical to the client's address + pub client_identity: ed25519::PublicKey, + + #[serde(with = "nym_serde_helpers::base64")] + pub address_ciphertext: Vec, + + #[serde(with = "nym_serde_helpers::base64")] + pub nonce: Vec, + + pub request_unix_timestamp: u64, +} + +impl AuthenticateRequestContent { + fn new( + protocol_version: u8, + shared_key: &SharedGatewayKey, + client_identity: ed25519::PublicKey, + ) -> Result { + let nonce = shared_key.random_nonce_or_iv(); + let destination_address = client_identity.derive_destination_address(); + + let address_ciphertext = + shared_key.encrypt(destination_address.as_bytes_ref(), Some(&nonce))?; + let now = OffsetDateTime::now_utc(); + Ok(AuthenticateRequestContent { + protocol_version, + client_identity, + address_ciphertext, + nonce, + request_unix_timestamp: now.unix_timestamp() as u64, // SAFETY: we're running this in post 1970... + }) + } +} + +impl AuthenticateRequestContent { + pub fn plaintext(&self) -> Vec { + iter::once(self.protocol_version) + .chain(self.client_identity.to_bytes()) + .chain(self.address_ciphertext.iter().copied()) + .chain(self.nonce.iter().copied()) + .chain(self.request_unix_timestamp.to_be_bytes()) + .collect() + } + + pub fn request_timestamp(&self) -> OffsetDateTime { + OffsetDateTime::from_unix_timestamp(self.request_unix_timestamp as i64) + .unwrap_or(OffsetDateTime::UNIX_EPOCH) + } +} diff --git a/common/gateway-requests/src/types/text_request.rs b/common/gateway-requests/src/types/text_request/mod.rs similarity index 89% rename from common/gateway-requests/src/types/text_request.rs rename to common/gateway-requests/src/types/text_request/mod.rs index 0fb864a533..7182dd834e 100644 --- a/common/gateway-requests/src/types/text_request.rs +++ b/common/gateway-requests/src/types/text_request/mod.rs @@ -2,16 +2,21 @@ // SPDX-License-Identifier: Apache-2.0 use crate::models::CredentialSpendingRequest; +use crate::text_request::authenticate::AuthenticateRequest; use crate::{ GatewayRequestsError, SharedGatewayKey, SymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION, - CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, + AUTHENTICATE_V2_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, + INITIAL_PROTOCOL_VERSION, }; use nym_credentials_interface::CredentialSpendingData; +use nym_crypto::asymmetric::ed25519; use nym_sphinx::DestinationAddressBytes; use serde::{Deserialize, Serialize}; use std::str::FromStr; use tungstenite::Message; +pub mod authenticate; + // wrapper for all encrypted requests for ease of use #[derive(Serialize, Deserialize, Debug, Clone)] #[non_exhaustive] @@ -68,6 +73,9 @@ pub enum ClientControlRequest { enc_address: String, iv: String, }, + + AuthenticateV2(Box), + #[serde(alias = "handshakePayload")] RegisterHandshakeInitRequest { #[serde(default)] @@ -123,9 +131,22 @@ impl ClientControlRequest { }) } + pub fn new_authenticate_v2( + shared_key: &SharedGatewayKey, + identity_keys: &ed25519::KeyPair, + ) -> Result { + // if we're using v2 authentication, we must announce at least that protocol version + let protocol_version = AUTHENTICATE_V2_PROTOCOL_VERSION; + + Ok(ClientControlRequest::AuthenticateV2(Box::new( + AuthenticateRequest::new(protocol_version, shared_key, identity_keys)?, + ))) + } + pub fn name(&self) -> String { match self { ClientControlRequest::Authenticate { .. } => "Authenticate".to_string(), + ClientControlRequest::AuthenticateV2(..) => "AuthenticateV2".to_string(), ClientControlRequest::RegisterHandshakeInitRequest { .. } => { "RegisterHandshakeInitRequest".to_string() } diff --git a/common/gateway-storage/migrations/20250303120000_add_authentication_timestamp.sql b/common/gateway-storage/migrations/20250303120000_add_authentication_timestamp.sql new file mode 100644 index 0000000000..e211803e9e --- /dev/null +++ b/common/gateway-storage/migrations/20250303120000_add_authentication_timestamp.sql @@ -0,0 +1,7 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +ALTER TABLE shared_keys + ADD COLUMN last_used_authentication TIMESTAMP WITHOUT TIME ZONE; \ No newline at end of file diff --git a/common/gateway-storage/src/lib.rs b/common/gateway-storage/src/lib.rs index 5f289ec106..2d05d43fe7 100644 --- a/common/gateway-storage/src/lib.rs +++ b/common/gateway-storage/src/lib.rs @@ -200,6 +200,20 @@ impl GatewayStorage { Ok(()) } + pub async fn update_last_used_authentication_timestamp( + &self, + client_id: i64, + last_used_authentication_timestamp: OffsetDateTime, + ) -> Result<(), GatewayStorageError> { + self.shared_key_manager + .update_last_used_authentication_timestamp( + client_id, + last_used_authentication_timestamp, + ) + .await?; + Ok(()) + } + pub async fn get_client(&self, client_id: i64) -> Result, GatewayStorageError> { let client = self.client_manager.get_client(client_id).await?; Ok(client) diff --git a/common/gateway-storage/src/models.rs b/common/gateway-storage/src/models.rs index c623f72602..665b8fee47 100644 --- a/common/gateway-storage/src/models.rs +++ b/common/gateway-storage/src/models.rs @@ -14,13 +14,13 @@ pub struct Client { #[derive(FromRow)] pub struct PersistedSharedKeys { - #[allow(dead_code)] pub client_id: i64, #[allow(dead_code)] pub client_address_bs58: String, pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option, pub derived_aes256_gcm_siv_key: Option>, + pub last_used_authentication: Option, } impl TryFrom for SharedGatewayKey { diff --git a/common/gateway-storage/src/shared_keys.rs b/common/gateway-storage/src/shared_keys.rs index 9d17535fb2..a7ae832cac 100644 --- a/common/gateway-storage/src/shared_keys.rs +++ b/common/gateway-storage/src/shared_keys.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::models::PersistedSharedKeys; +use time::OffsetDateTime; #[derive(Clone)] pub(crate) struct SharedKeysManager { @@ -68,6 +69,22 @@ impl SharedKeysManager { Ok(()) } + pub(crate) async fn update_last_used_authentication_timestamp( + &self, + client_id: i64, + last_used: OffsetDateTime, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE shared_keys SET last_used_authentication = ? WHERE client_id = ?;", + last_used, + client_id + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + /// Tries to retrieve shared keys stored for the particular client. /// /// # Arguments @@ -77,13 +94,10 @@ impl SharedKeysManager { &self, client_address_bs58: &str, ) -> Result, sqlx::Error> { - sqlx::query_as!( - PersistedSharedKeys, - "SELECT * FROM shared_keys WHERE client_address_bs58 = ?", - client_address_bs58 - ) - .fetch_optional(&self.connection_pool) - .await + sqlx::query_as("SELECT * FROM shared_keys WHERE client_address_bs58 = ?") + .bind(client_address_bs58) + .fetch_optional(&self.connection_pool) + .await } /// Removes from the database shared keys derived with the particular client. diff --git a/gateway/src/config.rs b/gateway/src/config.rs index 02d58ad143..1f6db8f0b8 100644 --- a/gateway/src/config.rs +++ b/gateway/src/config.rs @@ -101,6 +101,9 @@ pub struct Debug { pub maximum_open_connections: usize, pub zk_nym_tickets: ZkNymTicketHandlerDebug, + + /// Defines the maximum age of a signed authentication request before it's deemed too stale to process. + pub maximum_auth_request_age: Duration, } #[derive(Debug, Clone)] diff --git a/gateway/src/node/client_handling/active_clients.rs b/gateway/src/node/client_handling/active_clients.rs index 4a3e55bef5..eeff95d907 100644 --- a/gateway/src/node/client_handling/active_clients.rs +++ b/gateway/src/node/client_handling/active_clients.rs @@ -6,11 +6,20 @@ use crate::node::client_handling::embedded_clients::LocalEmbeddedClientHandle; use dashmap::DashMap; use nym_sphinx::DestinationAddressBytes; use std::sync::Arc; +use time::OffsetDateTime; use tracing::warn; +#[derive(Clone)] +pub(crate) struct RemoteClientData { + // note, this does **NOT** indicate timestamp of when client connected + // it is (for v2 auth) timestamp the client **signed** when it created the request + pub(crate) session_request_timestamp: OffsetDateTime, + pub(crate) channels: ClientIncomingChannels, +} + enum ActiveClient { /// Handle to a remote client connected via a network socket. - Remote(ClientIncomingChannels), + Remote(RemoteClientData), /// Handle to a locally (inside the same process) running client. Embedded(LocalEmbeddedClientHandle), @@ -19,14 +28,14 @@ enum ActiveClient { impl ActiveClient { fn get_sender_ref(&self) -> &MixMessageSender { match self { - ActiveClient::Remote(remote) => &remote.mix_message_sender, + ActiveClient::Remote(remote) => &remote.channels.mix_message_sender, ActiveClient::Embedded(embedded) => &embedded.mix_message_sender, } } fn get_sender(&self) -> MixMessageSender { match self { - ActiveClient::Remote(remote) => remote.mix_message_sender.clone(), + ActiveClient::Remote(remote) => remote.channels.mix_message_sender.clone(), ActiveClient::Embedded(embedded) => embedded.mix_message_sender.clone(), } } @@ -78,18 +87,18 @@ impl ActiveClientsStore { pub(crate) fn get_remote_client( &self, address: DestinationAddressBytes, - ) -> Option { + ) -> Option { let entry = self.inner.get(&address)?; let handle = entry.value(); - let ActiveClient::Remote(channels) = handle else { + let ActiveClient::Remote(remote) = handle else { warn!("attempted to get a remote handle to a embedded network requester"); return None; }; // if the entry is stale, remove it from the map - if !channels.mix_message_sender.is_closed() { - Some(channels.clone()) + if !remote.channels.mix_message_sender.is_closed() { + Some(remote.clone()) } else { // drop the reference to the map to prevent deadlocks drop(entry); @@ -137,10 +146,14 @@ impl ActiveClientsStore { client: DestinationAddressBytes, handle: MixMessageSender, is_active_request_sender: IsActiveRequestSender, + session_request_timestamp: OffsetDateTime, ) { - let entry = ActiveClient::Remote(ClientIncomingChannels { - mix_message_sender: handle, - is_active_request_sender, + let entry = ActiveClient::Remote(RemoteClientData { + session_request_timestamp, + channels: ClientIncomingChannels { + mix_message_sender: handle, + is_active_request_sender, + }, }); if self.inner.insert(client, entry).is_some() { panic!("inserted a duplicate remote client") diff --git a/gateway/src/node/client_handling/websocket/common_state.rs b/gateway/src/node/client_handling/websocket/common_state.rs index 223c534b26..a571117895 100644 --- a/gateway/src/node/client_handling/websocket/common_state.rs +++ b/gateway/src/node/client_handling/websocket/common_state.rs @@ -9,15 +9,23 @@ use nym_mixnet_client::forwarder::MixForwardingSender; use nym_node_metrics::events::MetricEventsSender; use nym_node_metrics::NymNodeMetrics; use std::sync::Arc; +use std::time::Duration; + +#[derive(Clone)] +pub(crate) struct Config { + pub(crate) enforce_zk_nym: bool, + pub(crate) max_auth_request_age: Duration, + + pub(crate) bandwidth: BandwidthFlushingBehaviourConfig, +} // I can see this being possible expanded with say storage or client store #[derive(Clone)] pub(crate) struct CommonHandlerState { + pub(crate) cfg: Config, pub(crate) ecash_verifier: Arc, pub(crate) storage: GatewayStorage, pub(crate) local_identity: Arc, - pub(crate) only_coconut_credentials: bool, - pub(crate) bandwidth_cfg: BandwidthFlushingBehaviourConfig, pub(crate) metrics: NymNodeMetrics, pub(crate) metrics_sender: MetricEventsSender, pub(crate) outbound_mix_sender: MixForwardingSender, diff --git a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs index 6c39b613ce..16c5a42faa 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -194,8 +194,8 @@ impl AuthenticatedHandler { fresh.shared_state.storage.clone(), ClientBandwidth::new(bandwidth.into()), client.id, - fresh.shared_state.bandwidth_cfg, - fresh.shared_state.only_coconut_credentials, + fresh.shared_state.cfg.bandwidth, + fresh.shared_state.cfg.enforce_zk_nym, ), inner: fresh, client, diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index 818aea8fdb..4c0eb81f35 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -1,11 +1,13 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node::client_handling::active_clients::RemoteClientData; use crate::node::client_handling::websocket::common_state::CommonHandlerState; +use crate::node::client_handling::websocket::connection_handler::helpers::KeyWithAuthTimestamp; use crate::node::client_handling::websocket::connection_handler::INITIAL_MESSAGE_TIMEOUT; use crate::node::client_handling::websocket::{ connection_handler::{AuthenticatedHandler, ClientDetails, InitialAuthResult, SocketStream}, - message_receiver::{IsActive, IsActiveRequestSender}, + message_receiver::IsActive, }; use futures::{ channel::{mpsc, oneshot}, @@ -13,14 +15,16 @@ use futures::{ }; use nym_credentials_interface::AvailableBandwidth; use nym_crypto::aes::cipher::crypto_common::rand_core::RngCore; -use nym_crypto::asymmetric::identity; +use nym_crypto::asymmetric::ed25519; +use nym_gateway_requests::authenticate::AuthenticateRequest; use nym_gateway_requests::authentication::encrypted_address::{ EncryptedAddressBytes, EncryptedAddressConversionError, }; use nym_gateway_requests::{ registration::handshake::{error::HandshakeError, gateway_handshake}, types::{ClientControlRequest, ServerResponse}, - BinaryResponse, SharedGatewayKey, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, + AuthenticationFailure, BinaryResponse, SharedGatewayKey, CURRENT_PROTOCOL_VERSION, + INITIAL_PROTOCOL_VERSION, }; use nym_gateway_storage::error::GatewayStorageError; use nym_node_metrics::events::MetricsEvent; @@ -30,6 +34,7 @@ use rand::CryptoRng; use std::net::SocketAddr; use std::time::Duration; use thiserror::Error; +use time::OffsetDateTime; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::time::timeout; use tokio_tungstenite::tungstenite::{protocol::Message, Error as WsError}; @@ -37,6 +42,12 @@ use tracing::*; #[derive(Debug, Error)] pub(crate) enum InitialAuthenticationError { + #[error(transparent)] + AuthenticationFailure(#[from] AuthenticationFailure), + + #[error("attempted to overwrite client session with a stale authentication")] + StaleSessionOverwrite, + #[error("Internal gateway storage error")] StorageError(#[from] GatewayStorageError), @@ -290,15 +301,15 @@ impl FreshHandler { // of doing full parse of the init_data elsewhere fn extract_remote_identity_from_register_init( init_data: &[u8], - ) -> Result { - if init_data.len() < identity::PUBLIC_KEY_LENGTH { + ) -> Result { + if init_data.len() < ed25519::PUBLIC_KEY_LENGTH { Err(InitialAuthenticationError::HandshakeError( HandshakeError::MalformedRequest, )) } else { - identity::PublicKey::from_bytes(&init_data[..identity::PUBLIC_KEY_LENGTH]).map_err( - |_| InitialAuthenticationError::HandshakeError(HandshakeError::MalformedRequest), - ) + ed25519::PublicKey::from_bytes(&init_data[..ed25519::PUBLIC_KEY_LENGTH]).map_err(|_| { + InitialAuthenticationError::HandshakeError(HandshakeError::MalformedRequest) + }) } } @@ -351,6 +362,21 @@ impl FreshHandler { Ok(()) } + async fn retrieve_shared_key( + &self, + client: DestinationAddressBytes, + ) -> Result, InitialAuthenticationError> { + let shared_keys = self.shared_state.storage.get_shared_keys(client).await?; + + let Some(stored_shared_keys) = shared_keys else { + return Ok(None); + }; + + let keys = KeyWithAuthTimestamp::try_from_stored(stored_shared_keys, client)?; + + Ok(Some(keys)) + } + /// Checks whether the stored shared keys match the received data, i.e. whether the upon decryption /// the provided encrypted address matches the expected unencrypted address. /// @@ -361,31 +387,18 @@ impl FreshHandler { /// * `client_address`: address of the client. /// * `encrypted_address`: encrypted address of the client, presumably encrypted using the shared keys. /// * `iv`: nonce/iv created for this particular encryption. - async fn verify_stored_shared_key( + async fn auth_v1_verify_stored_shared_key( &self, client_address: DestinationAddressBytes, encrypted_address: EncryptedAddressBytes, nonce: &[u8], - ) -> Result, InitialAuthenticationError> { - let shared_keys = self - .shared_state - .storage - .get_shared_keys(client_address) - .await?; - - let Some(stored_shared_keys) = shared_keys else { + ) -> Result, InitialAuthenticationError> { + let Some(keys) = self.retrieve_shared_key(client_address).await? else { return Ok(None); }; - let keys = SharedGatewayKey::try_from(stored_shared_keys).map_err(|source| { - InitialAuthenticationError::MalformedStoredSharedKey { - client_id: client_address.as_base58_string(), - source, - } - })?; - // LEGACY ISSUE: we're not verifying HMAC key - if encrypted_address.verify(&client_address, &keys, nonce) { + if encrypted_address.verify(&client_address, &keys.key, nonce) { Ok(Some(keys)) } else { Ok(None) @@ -428,49 +441,19 @@ impl FreshHandler { } } - /// Using the received challenge data, i.e. client's address as well the ciphertext of it plus - /// a fresh IV, attempts to authenticate the client by checking whether the ciphertext matches - /// the expected value if encrypted with the shared key. - /// - /// Finally, upon completion, all previously stored messages are pushed back to the client. - /// - /// # Arguments - /// - /// * `client_address`: address of the client wishing to authenticate. - /// * `encrypted_address`: ciphertext of the address of the client wishing to authenticate. - /// * `iv`: fresh nonce/IV received with the request. - async fn authenticate_client( - &mut self, - client_address: DestinationAddressBytes, - encrypted_address: EncryptedAddressBytes, - nonce: &[u8], - ) -> Result, InitialAuthenticationError> - where - S: AsyncRead + AsyncWrite + Unpin, - { - debug!( - "Processing authenticate client request for: {}", - client_address.as_base58_string() - ); - - let shared_keys = self - .verify_stored_shared_key(client_address, encrypted_address, nonce) - .await?; - - if let Some(shared_keys) = shared_keys { - self.push_stored_messages_to_client(client_address, &shared_keys) - .await?; - Ok(Some(shared_keys)) - } else { - Ok(None) - } - } - async fn handle_duplicate_client( &mut self, address: DestinationAddressBytes, - mut is_active_request_tx: IsActiveRequestSender, + remote_client_data: RemoteClientData, + new_session_start: OffsetDateTime, ) -> Result<(), InitialAuthenticationError> { + let mut is_active_request_tx = remote_client_data.channels.is_active_request_sender; + + // new session must **always** be explicitly more recent + if new_session_start <= remote_client_data.session_request_timestamp { + return Err(InitialAuthenticationError::StaleSessionOverwrite); + } + // Ask the other connection to ping if they are still active. // Use a oneshot channel to return the result to us let (ping_result_sender, ping_result_receiver) = oneshot::channel(); @@ -519,6 +502,32 @@ impl FreshHandler { Ok(()) } + #[allow(dead_code)] + async fn get_registered_client_id( + &self, + client_address: DestinationAddressBytes, + ) -> Result { + self.shared_state + .storage + .get_mixnet_client_id(client_address) + .await + .map_err(Into::into) + } + + async fn get_registered_available_bandwidth( + &self, + client_id: i64, + ) -> Result { + let available_bandwidth: AvailableBandwidth = self + .shared_state + .storage + .get_available_bandwidth(client_id) + .await? + .map(From::from) + .unwrap_or_default(); + Ok(available_bandwidth) + } + /// Tries to handle the received authentication request by checking correctness of the received data. /// /// # Arguments @@ -531,7 +540,7 @@ impl FreshHandler { address = %address, ) )] - async fn handle_authenticate( + async fn handle_legacy_authenticate( &mut self, client_protocol_version: Option, address: String, @@ -541,7 +550,7 @@ impl FreshHandler { where S: AsyncRead + AsyncWrite + Unpin, { - debug!("handling client registration"); + debug!("handling client authentication (v1)"); let negotiated_protocol = self.negotiate_client_protocol(client_protocol_version)?; // populate the negotiated protocol for future uses @@ -554,38 +563,38 @@ impl FreshHandler { .into_vec() .map_err(InitialAuthenticationError::MalformedIV)?; - // Check for duplicate clients - if let Some(client_tx) = self - .shared_state - .active_clients_store - .get_remote_client(address) - { - warn!("Detected duplicate connection for client: {address}"); - self.handle_duplicate_client(address, client_tx.is_active_request_sender) - .await?; - } - + // validate the shared key let Some(shared_keys) = self - .authenticate_client(address, encrypted_address, &nonce) + .auth_v1_verify_stored_shared_key(address, encrypted_address, &nonce) .await? else { // it feels weird to be returning an 'Ok' here, but I didn't want to change the existing behaviour return Ok(InitialAuthResult::new_failed(Some(negotiated_protocol))); }; - let client_id = self + // in v1 we don't have explicit data so we have to use current timestamp + // (which does nothing but just allows us to use the same codepath) + let session_request_start = OffsetDateTime::now_utc(); + + // Check for duplicate clients + if let Some(remote_client_data) = self .shared_state - .storage - .get_mixnet_client_id(address) + .active_clients_store + .get_remote_client(address) + { + warn!("Detected duplicate connection for client: {address}"); + self.handle_duplicate_client(address, remote_client_data, session_request_start) + .await?; + } + + let client_id = shared_keys.client_id; + + // if applicable, push stored messages + self.push_stored_messages_to_client(address, &shared_keys.key) .await?; - let available_bandwidth: AvailableBandwidth = self - .shared_state - .storage - .get_available_bandwidth(client_id) - .await? - .map(From::from) - .unwrap_or_default(); + // check the bandwidth + let available_bandwidth = self.get_registered_available_bandwidth(client_id).await?; let bandwidth_remaining = if available_bandwidth.expired() { self.shared_state.storage.reset_bandwidth(client_id).await?; @@ -595,7 +604,98 @@ impl FreshHandler { }; Ok(InitialAuthResult::new( - Some(ClientDetails::new(client_id, address, shared_keys)), + Some(ClientDetails::new( + client_id, + address, + shared_keys.key, + session_request_start, + )), + ServerResponse::Authenticate { + protocol_version: Some(negotiated_protocol), + status: true, + bandwidth_remaining, + }, + )) + } + + async fn handle_authenticate_v2( + &mut self, + request: Box, + ) -> Result + where + S: AsyncRead + AsyncWrite + Unpin, + { + debug!("handling client authentication (v2)"); + + let negotiated_protocol = + self.negotiate_client_protocol(Some(request.content.protocol_version))?; + // populate the negotiated protocol for future uses + self.negotiated_protocol = Some(negotiated_protocol); + + let address = request.content.client_identity.derive_destination_address(); + + // do cheap checks first + // is the provided timestamp relatively recent (and not in the future?) + request.verify_timestamp(self.shared_state.cfg.max_auth_request_age)?; + + // does the message signature verify? + request.verify_signature()?; + + // retrieve the actually stored key and check if the ciphertext matches + let Some(shared_key) = self.retrieve_shared_key(address).await? else { + return Err(AuthenticationFailure::NotRegistered)?; + }; + request.verify_ciphertext(&shared_key.key)?; + + let session_request_start = request.content.request_timestamp(); + + // if the client has already authenticated in the past, make sure this authentication timestamp + // is different and greater than the old one (in case it was replayed) + if let Some(prior_usage) = shared_key.last_used_authentication { + request.ensure_timestamp_not_reused(prior_usage)?; + } + + // check for duplicate clients + if let Some(client_data) = self + .shared_state + .active_clients_store + .get_remote_client(address) + { + warn!("Detected duplicate connection for client: {address}"); + self.handle_duplicate_client(address, client_data, session_request_start) + .await?; + } + + let client_id = shared_key.client_id; + + // update the auth timestamp for future uses + self.shared_state + .storage + .update_last_used_authentication_timestamp(client_id, session_request_start) + .await?; + + // push any old stored messages to the client + // (this will be removed soon) + self.push_stored_messages_to_client(address, &shared_key.key) + .await?; + + // finally check and retrieve client's bandwidth + let available_bandwidth = self.get_registered_available_bandwidth(client_id).await?; + + let bandwidth_remaining = if available_bandwidth.expired() { + self.shared_state.storage.reset_bandwidth(client_id).await?; + 0 + } else { + available_bandwidth.bytes + }; + + Ok(InitialAuthResult::new( + Some(ClientDetails::new( + client_id, + address, + shared_key.key, + session_request_start, + )), ServerResponse::Authenticate { protocol_version: Some(negotiated_protocol), status: true, @@ -688,7 +788,12 @@ impl FreshHandler { debug!(client_id = %client_id, "managed to finalize client registration"); - let client_details = ClientDetails::new(client_id, remote_address, shared_keys); + let client_details = ClientDetails::new( + client_id, + remote_address, + shared_keys, + OffsetDateTime::now_utc(), + ); Ok(InitialAuthResult::new( Some(client_details), @@ -734,9 +839,10 @@ impl FreshHandler { enc_address, iv, } => { - self.handle_authenticate(protocol_version, address, enc_address, iv) + self.handle_legacy_authenticate(protocol_version, address, enc_address, iv) .await } + ClientControlRequest::AuthenticateV2(req) => self.handle_authenticate_v2(req).await, ClientControlRequest::RegisterHandshakeInitRequest { protocol_version, data, @@ -827,6 +933,7 @@ impl FreshHandler { registration_details.address, mix_sender, is_active_request_sender, + registration_details.session_request_timestamp, ); return AuthenticatedHandler::upgrade( diff --git a/gateway/src/node/client_handling/websocket/connection_handler/helpers.rs b/gateway/src/node/client_handling/websocket/connection_handler/helpers.rs new file mode 100644 index 0000000000..5a5d24c5d6 --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/helpers.rs @@ -0,0 +1,37 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::client_handling::websocket::connection_handler::fresh::InitialAuthenticationError; +use nym_gateway_requests::SharedGatewayKey; +use nym_gateway_storage::models::PersistedSharedKeys; +use nym_sphinx::DestinationAddressBytes; +use time::OffsetDateTime; + +pub(crate) struct KeyWithAuthTimestamp { + pub(crate) client_id: i64, + pub(crate) key: SharedGatewayKey, + pub(crate) last_used_authentication: Option, +} + +impl KeyWithAuthTimestamp { + pub(crate) fn try_from_stored( + stored_shared_keys: PersistedSharedKeys, + client: DestinationAddressBytes, + ) -> Result { + let last_used_authentication = stored_shared_keys.last_used_authentication; + let client_id = stored_shared_keys.client_id; + + let key = SharedGatewayKey::try_from(stored_shared_keys).map_err(|source| { + InitialAuthenticationError::MalformedStoredSharedKey { + client_id: client.as_base58_string(), + source, + } + })?; + + Ok(KeyWithAuthTimestamp { + client_id, + key, + last_used_authentication, + }) + } +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs index f9f6fc584d..38026fa8be 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs @@ -8,16 +8,17 @@ use nym_gateway_requests::ServerResponse; use nym_sphinx::DestinationAddressBytes; use rand::{CryptoRng, Rng}; use std::time::Duration; +use time::OffsetDateTime; use tokio::io::{AsyncRead, AsyncWrite}; use tokio_tungstenite::WebSocketStream; use tracing::{debug, instrument, trace, warn}; -use zeroize::{Zeroize, ZeroizeOnDrop}; pub(crate) use self::authenticated::AuthenticatedHandler; pub(crate) use self::fresh::FreshHandler; pub(crate) mod authenticated; mod fresh; +pub(crate) mod helpers; const WEBSOCKET_HANDSHAKE_TIMEOUT: Duration = Duration::from_millis(1_500); const INITIAL_MESSAGE_TIMEOUT: Duration = Duration::from_millis(10_000); @@ -40,12 +41,13 @@ impl SocketStream { } } -#[derive(Zeroize, ZeroizeOnDrop)] pub(crate) struct ClientDetails { - #[zeroize(skip)] pub(crate) address: DestinationAddressBytes, pub(crate) id: i64, pub(crate) shared_keys: SharedGatewayKey, + // note, this does **NOT ALWAYS** indicate timestamp of when client connected + // it is (for v2 auth) timestamp the client **signed** when it created the request + pub(crate) session_request_timestamp: OffsetDateTime, } impl ClientDetails { @@ -53,11 +55,13 @@ impl ClientDetails { id: i64, address: DestinationAddressBytes, shared_keys: SharedGatewayKey, + session_request_timestamp: OffsetDateTime, ) -> Self { ClientDetails { address, id, shared_keys, + session_request_timestamp, } } } diff --git a/gateway/src/node/client_handling/websocket/mod.rs b/gateway/src/node/client_handling/websocket/mod.rs index 12c252feca..2405870a42 100644 --- a/gateway/src/node/client_handling/websocket/mod.rs +++ b/gateway/src/node/client_handling/websocket/mod.rs @@ -8,4 +8,4 @@ pub(crate) mod connection_handler; pub(crate) mod listener; pub(crate) mod message_receiver; -pub(crate) use common_state::CommonHandlerState; +pub(crate) use common_state::{CommonHandlerState, Config}; diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 9323bb572b..a582a24735 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -249,11 +249,14 @@ impl GatewayTasksBuilder { active_clients_store: ActiveClientsStore, ) -> Result { let shared_state = websocket::CommonHandlerState { + cfg: websocket::Config { + enforce_zk_nym: self.config.gateway.enforce_zk_nyms, + max_auth_request_age: self.config.debug.maximum_auth_request_age, + bandwidth: (&self.config).into(), + }, ecash_verifier: self.ecash_manager().await?, storage: self.storage.clone(), local_identity: Arc::clone(&self.identity_keypair), - only_coconut_credentials: self.config.gateway.enforce_zk_nyms, - bandwidth_cfg: (&self.config).into(), metrics: self.metrics.clone(), metrics_sender: self.metrics_sender.clone(), outbound_mix_sender: self.mix_packet_sender.clone(), diff --git a/nym-node/src/config/gateway_tasks.rs b/nym-node/src/config/gateway_tasks.rs index 1a4c7454ac..97a086177e 100644 --- a/nym-node/src/config/gateway_tasks.rs +++ b/nym-node/src/config/gateway_tasks.rs @@ -50,6 +50,9 @@ pub struct Debug { /// The maximum number of client connections the gateway will keep open at once. pub maximum_open_connections: usize, + /// Defines the maximum age of a signed authentication request before it's deemed too stale to process. + pub maximum_auth_request_age: Duration, + pub stale_messages: StaleMessageDebug, pub client_bandwidth: ClientBandwidthDebug, @@ -58,8 +61,9 @@ pub struct Debug { } impl Debug { - const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; - const DEFAULT_MAXIMUM_OPEN_CONNECTIONS: usize = 8192; + pub const DEFAULT_MAXIMUM_OPEN_CONNECTIONS: usize = 8192; + pub const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; + pub const DEFAULT_MAXIMUM_AUTH_REQUEST_AGE: Duration = Duration::from_secs(30); } impl Default for Debug { @@ -67,6 +71,7 @@ impl Default for Debug { Debug { message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, maximum_open_connections: Self::DEFAULT_MAXIMUM_OPEN_CONNECTIONS, + maximum_auth_request_age: Self::DEFAULT_MAXIMUM_AUTH_REQUEST_AGE, stale_messages: Default::default(), client_bandwidth: Default::default(), zk_nym_tickets: Default::default(), diff --git a/nym-node/src/config/helpers.rs b/nym-node/src/config/helpers.rs index f3380ef223..a56e0442e0 100644 --- a/nym-node/src/config/helpers.rs +++ b/nym-node/src/config/helpers.rs @@ -60,6 +60,7 @@ fn ephemeral_gateway_config(config: &Config) -> nym_gateway::config::Config { .zk_nym_tickets .maximum_time_between_redemption, }, + maximum_auth_request_age: config.gateway_tasks.debug.maximum_auth_request_age, }, ) } From 7335a3dad42be09107c267a365247e5df49aa70a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 6 Mar 2025 11:08:52 +0000 Subject: [PATCH 09/18] fix: gateway protocol negotation for v3/v4 --- .../client_handling/websocket/connection_handler/fresh.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index 4c0eb81f35..bd1f9fd601 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -426,6 +426,11 @@ impl FreshHandler { return Ok(2); } + // a v4 gateway will understand v3 requests (aes256gcm-siv) + if client_protocol_version == 3 { + return Ok(3); + } + // we can't handle clients with higher protocol than ours // (perhaps we could try to negotiate downgrade on our end? sounds like a nice future improvement) if client_protocol_version <= CURRENT_PROTOCOL_VERSION { From 35bf49c48cbf9d4b08e059dea569ff44f2a1923d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 6 Mar 2025 11:24:03 +0000 Subject: [PATCH 10/18] chore: additional logs when attempting to load ecash keys (#5567) --- common/pemstore/Cargo.toml | 1 + common/pemstore/src/lib.rs | 5 +++++ nym-api/src/ecash/dkg/controller/keys.rs | 17 ++++++++++------- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/common/pemstore/Cargo.toml b/common/pemstore/Cargo.toml index f3bc33a6ef..f8d13391dd 100644 --- a/common/pemstore/Cargo.toml +++ b/common/pemstore/Cargo.toml @@ -9,3 +9,4 @@ repository = { workspace = true } [dependencies] pem = { workspace = true } +tracing = { workspace = true } \ No newline at end of file diff --git a/common/pemstore/src/lib.rs b/common/pemstore/src/lib.rs index bd36158229..9f6d3e283b 100644 --- a/common/pemstore/src/lib.rs +++ b/common/pemstore/src/lib.rs @@ -6,6 +6,7 @@ use pem::Pem; use std::fs::File; use std::io::{self, Read, Write}; use std::path::{Path, PathBuf}; +use tracing::debug; pub mod traits; @@ -46,6 +47,10 @@ where T: PemStorableKey, P: AsRef, { + debug!( + "attempting to load key with the following pem type: {}", + T::pem_type() + ); let key_pem = read_pem_file(path)?; if T::pem_type() != key_pem.tag { diff --git a/nym-api/src/ecash/dkg/controller/keys.rs b/nym-api/src/ecash/dkg/controller/keys.rs index bfbaa8c28d..f0038e4869 100644 --- a/nym-api/src/ecash/dkg/controller/keys.rs +++ b/nym-api/src/ecash/dkg/controller/keys.rs @@ -10,7 +10,7 @@ use nym_dkg::bte::keys::KeyPair as DkgKeyPair; use rand::{CryptoRng, RngCore}; use std::path::Path; use thiserror::__private::AsDisplay; -use tracing::warn; +use tracing::{debug, warn}; pub(crate) fn init_bte_keypair( rng: &mut R, @@ -39,17 +39,20 @@ pub(crate) fn load_bte_keypair(config: &config::EcashSigner) -> anyhow::Result anyhow::Result> { + let storage_path = &config.storage_paths.ecash_key_path; + debug!( + "attempting to ecash keypair from {}", + storage_path.display() + ); if !config.storage_paths.ecash_key_path.exists() { + debug!("the provided filepath doesn't exist - the key won't be loaded"); return Ok(None); } - if let Ok(ecash_key) = + let ecash_key = nym_pemstore::load_key::(&config.storage_paths.ecash_key_path) - { - return Ok(Some(ecash_key)); - } - - bail!("ecash key load failure") + .context("failed to load ecash key")?; + Ok(Some(ecash_key)) } // the keys can be considered valid if they were generated for the current dkg epoch From be92ccf0da84d3797feac00b9586ae8e21243046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 6 Mar 2025 11:24:16 +0000 Subject: [PATCH 11/18] bugfix: make sure to correctly decode response content when putting it into error message (#5571) --- Cargo.lock | 9 ++++++--- Cargo.toml | 1 + common/http-api-client/Cargo.toml | 8 +++++++- common/http-api-client/src/lib.rs | 30 ++++++++++++++++++++++++++---- nym-wallet/Cargo.lock | 11 +++++++---- 5 files changed, 47 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f75819cd5..277779ad57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2291,9 +2291,9 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -5767,7 +5767,7 @@ dependencies = [ "serde", "serde_json", "strum 0.26.3", - "subtle 2.6.1", + "subtle 2.5.0", "thiserror 2.0.11", "time", "tokio", @@ -5841,8 +5841,11 @@ name = "nym-http-api-client" version = "0.1.0" dependencies = [ "async-trait", + "bytes", + "encoding_rs", "hickory-resolver", "http 1.2.0", + "mime", "nym-bin-common", "once_cell", "reqwest 0.12.4", diff --git a/Cargo.toml b/Cargo.toml index 14e86aadfc..5452485c28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -241,6 +241,7 @@ doc-comment = "0.3" dotenvy = "0.15.6" ecdsa = "0.16" ed25519-dalek = "2.1" +encoding_rs = "0.8.35" env_logger = "0.11.6" envy = "0.4" etherparse = "0.13.0" diff --git a/common/http-api-client/Cargo.toml b/common/http-api-client/Cargo.toml index 33ff7bc6f8..79a291e827 100644 --- a/common/http-api-client/Cargo.toml +++ b/common/http-api-client/Cargo.toml @@ -21,6 +21,12 @@ serde_json = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } +# used for decoding text responses (they were already implicitly included) +bytes = { workspace = true } +encoding_rs = { workspace = true } +mime = { workspace = true } + + nym-bin-common = { path = "../bin-common" } [target."cfg(not(target_arch = \"wasm32\"))".dependencies] @@ -32,4 +38,4 @@ workspace = true features = ["tokio"] [dev-dependencies] -tokio = { workspace = true, features=["rt", "macros"] } +tokio = { workspace = true, features = ["rt", "macros"] } diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index a8099ce111..4b2938daa4 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -147,13 +147,13 @@ use thiserror::Error; use tracing::{instrument, warn}; use url::Url; +use http::HeaderMap; +pub use reqwest::IntoUrl; #[cfg(not(target_arch = "wasm32"))] use std::net::SocketAddr; #[cfg(not(target_arch = "wasm32"))] use std::sync::Arc; -pub use reqwest::IntoUrl; - mod user_agent; pub use user_agent::UserAgent; @@ -855,6 +855,26 @@ fn sanitize_url, V: AsRef>( url } +fn decode_as_text(bytes: &bytes::Bytes, headers: HeaderMap) -> String { + use encoding_rs::{Encoding, UTF_8}; + use mime::Mime; + + let content_type = headers + .get(http::header::CONTENT_TYPE) + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.parse::().ok()); + + let encoding_name = content_type + .as_ref() + .and_then(|mime| mime.get_param("charset").map(|charset| charset.as_str())) + .unwrap_or("utf-8"); + + let encoding = Encoding::for_label(encoding_name.as_bytes()).unwrap_or(UTF_8); + + let (text, _, _) = encoding.decode(bytes); + text.into_owned() +} + /// Attempt to parse a json object from an HTTP response #[instrument(level = "debug", skip_all)] pub async fn parse_response(res: Response, allow_empty: bool) -> Result> @@ -870,6 +890,8 @@ where return Err(HttpClientError::EmptyResponse { status }); } } + let headers = res.headers().clone(); + tracing::trace!("headers: {:?}", headers); if res.status().is_success() { // internally reqwest is first retrieving bytes and then performing parsing via serde_json @@ -878,10 +900,10 @@ where match serde_json::from_slice(&full) { Ok(data) => Ok(data), Err(err) => { - let text = String::from_utf8_lossy(&full); + let content = decode_as_text(&full, headers); Err(HttpClientError::ResponseDecodeFailure { source: err, - content: text.into_owned(), + content, }) } } diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 7fb3e6a067..969932de63 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -465,9 +465,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" dependencies = [ "serde", ] @@ -1487,9 +1487,9 @@ checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -3454,8 +3454,11 @@ name = "nym-http-api-client" version = "0.1.0" dependencies = [ "async-trait", + "bytes", + "encoding_rs", "hickory-resolver", "http 1.1.0", + "mime", "nym-bin-common", "once_cell", "reqwest 0.12.4", From c33e4c083612ae216407932bde5333799250a0d9 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Thu, 6 Mar 2025 15:03:43 +0100 Subject: [PATCH 12/18] bumping versions dorina patched --- Cargo.lock | 13 +++++++------ clients/native/Cargo.toml | 2 +- clients/socks5/Cargo.toml | 2 +- explorer-api/Cargo.toml | 2 +- service-providers/network-requester/Cargo.toml | 2 +- tools/nym-cli/Cargo.toml | 2 +- tools/nymvisor/Cargo.toml | 2 +- 7 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 277779ad57..b018fc913f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2420,7 +2420,7 @@ dependencies = [ [[package]] name = "explorer-api" -version = "1.1.47" +version = "1.1.48" dependencies = [ "chrono", "clap", @@ -4996,7 +4996,7 @@ dependencies = [ [[package]] name = "nym-cli" -version = "1.1.49" +version = "1.1.50" dependencies = [ "anyhow", "base64 0.22.1", @@ -5079,7 +5079,7 @@ dependencies = [ [[package]] name = "nym-client" -version = "1.1.49" +version = "1.1.50" dependencies = [ "bs58", "clap", @@ -6123,7 +6123,7 @@ dependencies = [ [[package]] name = "nym-network-requester" -version = "1.1.50" +version = "1.1.51" dependencies = [ "addr", "anyhow", @@ -6455,6 +6455,7 @@ name = "nym-pemstore" version = "0.3.0" dependencies = [ "pem", + "tracing", ] [[package]] @@ -6551,7 +6552,7 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.1.49" +version = "1.1.50" dependencies = [ "bs58", "clap", @@ -7156,7 +7157,7 @@ dependencies = [ [[package]] name = "nymvisor" -version = "0.1.14" +version = "0.1.15" dependencies = [ "anyhow", "bytes", diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index a3dfdc0661..9663b41d78 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-client" -version = "1.1.49" +version = "1.1.50" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Client" edition = "2021" diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index 934c0b6db9..9c4b1522ca 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-socks5-client" -version = "1.1.49" +version = "1.1.50" authors = ["Dave Hrycyszyn "] description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address" edition = "2021" diff --git a/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index 585dfd47bf..6bff50d87a 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "explorer-api" -version = "1.1.47" +version = "1.1.48" edition = "2021" license.workspace = true diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index bbc7080819..207f3960c2 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-network-requester" license = "GPL-3.0" -version = "1.1.50" +version = "1.1.51" authors.workspace = true edition.workspace = true rust-version = "1.70" diff --git a/tools/nym-cli/Cargo.toml b/tools/nym-cli/Cargo.toml index 9163feeb0b..a9b24b4ced 100644 --- a/tools/nym-cli/Cargo.toml +++ b/tools/nym-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-cli" -version = "1.1.49" +version = "1.1.50" authors.workspace = true edition = "2021" license.workspace = true diff --git a/tools/nymvisor/Cargo.toml b/tools/nymvisor/Cargo.toml index 57b4de306d..15de5e1610 100644 --- a/tools/nymvisor/Cargo.toml +++ b/tools/nymvisor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nymvisor" -version = "0.1.14" +version = "0.1.15" authors.workspace = true repository.workspace = true homepage.workspace = true From f6b30d0db681b9583e87b2547f3da60797d8ccfb Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Thu, 6 Mar 2025 15:06:24 +0100 Subject: [PATCH 13/18] update changelog for patched-dorina --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50bad8f2cf..785601d969 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ## [Unreleased] +## [2025.4-dorina-patched] (2025-03-06) + +- bugfix: make sure to correctly decode response content when putting it into error message ([#5571]) +- Tweak surb management to be more conservative ([#5570]) +- Deserialize v5 authenticator requests ([#5568]) +- chore: additional logs when attempting to load ecash keys ([#5567]) +- add full response body to error message upon decoding failure ([#5566]) +- hotfix: ensure we bail on merkle leaves insertion upon missing data ([#5565]) +- feature: v2 authentication request (#5537) ([#5563]) +- Create authenticator v5 request/response types ([#5561]) + +[#5571]: https://github.com/nymtech/nym/pull/5571 +[#5570]: https://github.com/nymtech/nym/pull/5570 +[#5568]: https://github.com/nymtech/nym/pull/5568 +[#5567]: https://github.com/nymtech/nym/pull/5567 +[#5566]: https://github.com/nymtech/nym/pull/5566 +[#5565]: https://github.com/nymtech/nym/pull/5565 +[#5563]: https://github.com/nymtech/nym/pull/5563 +[#5561]: https://github.com/nymtech/nym/pull/5561 + ## [2025.4-dorina] (2025-03-04) - fixed sphinx version metrics registration ([#5546]) From b42e5b063e0d5a00bb16fb05ddec8e294707c402 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Thu, 6 Mar 2025 15:45:02 +0100 Subject: [PATCH 14/18] bump api version --- Cargo.lock | 2 +- nym-api/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b018fc913f..ef32d6684a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4747,7 +4747,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "nym-api" -version = "1.1.52" +version = "1.1.53" dependencies = [ "anyhow", "async-trait", diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 239b483bf2..aa6b966b7f 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-api" license = "GPL-3.0" -version = "1.1.52" +version = "1.1.53" authors.workspace = true edition = "2021" rust-version.workspace = true From 430c33eb04656a931a38f0e44eea19ae39aaf3f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 6 Mar 2025 15:26:18 +0100 Subject: [PATCH 15/18] Set DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE to 50 --- common/client-core/config-types/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/client-core/config-types/src/lib.rs b/common/client-core/config-types/src/lib.rs index 82e93f9b09..5930245e51 100644 --- a/common/client-core/config-types/src/lib.rs +++ b/common/client-core/config-types/src/lib.rs @@ -50,7 +50,7 @@ const DEFAULT_MINIMUM_REPLY_SURB_THRESHOLD_BUFFER: usize = 0; // define how much to request at once // clients/client-core/src/client/replies/reply_controller.rs const DEFAULT_MINIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 10; -const DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 30; +const DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 50; const DEFAULT_MAXIMUM_ALLOWED_SURB_REQUEST_SIZE: u32 = 500; From 6bd31b9521de7baf04dfebb9e427eee61df46bae Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Thu, 6 Mar 2025 18:08:58 +0100 Subject: [PATCH 16/18] bump nym-node version --- Cargo.lock | 2 +- nym-node/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef32d6684a..fe70c9ce8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6174,7 +6174,7 @@ dependencies = [ [[package]] name = "nym-node" -version = "1.6.1" +version = "1.6.2" dependencies = [ "anyhow", "arc-swap", diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 028db3681a..0cf816ba5b 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node" -version = "1.6.1" +version = "1.6.2" authors.workspace = true repository.workspace = true homepage.workspace = true From 01c052e9a455d1396104b158ef869242d160fc5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 6 Mar 2025 20:13:16 +0000 Subject: [PATCH 17/18] use legacy crypto for constructing SURB headers (#5579) --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- common/nymsphinx/anonymous-replies/src/reply_surb.rs | 5 +++-- nym-wallet/Cargo.lock | 1 + 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe70c9ce8c..1d41821313 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9414,9 +9414,9 @@ dependencies = [ [[package]] name = "sphinx-packet" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535f2c430778bf59c22249fcc1ed6d384129eb2f0f694706015d636c688f9ac6" +checksum = "c23047e0cf36ff6904603f499fd13153425cdf5ba47bfbaedbc999da0bd92f4e" dependencies = [ "aes", "arrayref", diff --git a/Cargo.toml b/Cargo.toml index 5452485c28..80d483f003 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -322,7 +322,7 @@ serde_with = "3.9.0" serde_yaml = "0.9.25" sha2 = "0.10.8" si-scale = "0.2.3" -sphinx-packet = "0.3.1" +sphinx-packet = "=0.3.2" sqlx = "0.7.4" strum = "0.26" strum_macros = "0.26" diff --git a/common/nymsphinx/anonymous-replies/src/reply_surb.rs b/common/nymsphinx/anonymous-replies/src/reply_surb.rs index 90bd892d78..a6729e7e5d 100644 --- a/common/nymsphinx/anonymous-replies/src/reply_surb.rs +++ b/common/nymsphinx/anonymous-replies/src/reply_surb.rs @@ -7,7 +7,7 @@ use nym_sphinx_addressing::clients::Recipient; use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, MAX_NODE_ADDRESS_UNPADDED_LEN}; use nym_sphinx_params::packet_sizes::PacketSize; use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm}; -use nym_sphinx_types::{NymPacket, SURBMaterial, SphinxError, SURB}; +use nym_sphinx_types::{NymPacket, SURBMaterial, SphinxError, SURB, UPDATED_LEGACY_VERSION}; use nym_topology::{NymRouteProvider, NymTopologyError}; use rand::{CryptoRng, RngCore}; use serde::de::{Error as SerdeError, Visitor}; @@ -101,7 +101,8 @@ impl ReplySurb { let delays = nym_sphinx_routing::generate_hop_delays(average_delay, route.len()); let destination = recipient.as_sphinx_destination(); - let surb_material = SURBMaterial::new(route, delays, destination); + let surb_material = + SURBMaterial::new(route, delays, destination).with_version(UPDATED_LEGACY_VERSION); // this can't fail as we know we have a valid route to gateway and have correct number of delays Ok(ReplySurb { diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 969932de63..e92a03c300 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3548,6 +3548,7 @@ name = "nym-pemstore" version = "0.3.0" dependencies = [ "pem", + "tracing", ] [[package]] From 247ebb7c4339de0a298a7fcb2574122a8306c3b8 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Thu, 6 Mar 2025 21:26:16 +0100 Subject: [PATCH 18/18] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 785601d969..6c4e17091f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ## [2025.4-dorina-patched] (2025-03-06) +- use legacy crypto for constructing SURB headers ([#5579]) - bugfix: make sure to correctly decode response content when putting it into error message ([#5571]) - Tweak surb management to be more conservative ([#5570]) - Deserialize v5 authenticator requests ([#5568]) @@ -15,6 +16,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - feature: v2 authentication request (#5537) ([#5563]) - Create authenticator v5 request/response types ([#5561]) +[#5579]: https://github.com/nymtech/nym/pull/5579 [#5571]: https://github.com/nymtech/nym/pull/5571 [#5570]: https://github.com/nymtech/nym/pull/5570 [#5568]: https://github.com/nymtech/nym/pull/5568