From d21eff3b7ff11dd4bbe7eded2786c1661e759a23 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Tue, 20 May 2025 16:03:47 +0200 Subject: [PATCH 01/21] add noise lib --- Cargo.lock | 45 +++++++ Cargo.toml | 9 +- common/nymnoise/Cargo.toml | 23 ++++ common/nymnoise/keys/Cargo.toml | 14 +++ common/nymnoise/keys/src/lib.rs | 40 +++++++ common/nymnoise/src/config.rs | 132 +++++++++++++++++++++ common/nymnoise/src/connection.rs | 72 +++++++++++ common/nymnoise/src/error.rs | 36 ++++++ common/nymnoise/src/lib.rs | 158 ++++++++++++++++++++++++ common/nymnoise/src/stream.rs | 191 ++++++++++++++++++++++++++++++ 10 files changed, 718 insertions(+), 2 deletions(-) create mode 100644 common/nymnoise/Cargo.toml create mode 100644 common/nymnoise/keys/Cargo.toml create mode 100644 common/nymnoise/keys/src/lib.rs create mode 100644 common/nymnoise/src/config.rs create mode 100644 common/nymnoise/src/connection.rs create mode 100644 common/nymnoise/src/error.rs create mode 100644 common/nymnoise/src/lib.rs create mode 100644 common/nymnoise/src/stream.rs diff --git a/Cargo.lock b/Cargo.lock index 7b663ebae4e..0838876774f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6411,6 +6411,35 @@ dependencies = [ "wasmtimer", ] +[[package]] +name = "nym-noise" +version = "0.1.0" +dependencies = [ + "arc-swap", + "bytes", + "futures", + "log", + "nym-crypto", + "nym-noise-keys", + "pin-project", + "serde", + "sha2 0.10.8", + "snow", + "thiserror 2.0.12", + "tokio", + "tokio-util", +] + +[[package]] +name = "nym-noise-keys" +version = "0.1.0" +dependencies = [ + "nym-crypto", + "schemars", + "serde", + "utoipa", +] + [[package]] name = "nym-nonexhaustive-delayqueue" version = "0.1.0" @@ -9276,6 +9305,22 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "snow" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85" +dependencies = [ + "aes-gcm", + "blake2 0.10.6", + "chacha20poly1305", + "curve25519-dalek", + "rand_core 0.6.4", + "rustc_version 0.4.1", + "sha2 0.10.8", + "subtle 2.6.1", +] + [[package]] name = "socket2" version = "0.5.9" diff --git a/Cargo.toml b/Cargo.toml index cd9914aa612..139ed55e55b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,8 @@ members = [ "common/commands", "common/config", "common/cosmwasm-smart-contracts/coconut-dkg", - "common/cosmwasm-smart-contracts/contracts-common", "common/cosmwasm-smart-contracts/easy_addr", + "common/cosmwasm-smart-contracts/contracts-common", + "common/cosmwasm-smart-contracts/easy_addr", "common/cosmwasm-smart-contracts/ecash-contract", "common/cosmwasm-smart-contracts/group-contract", "common/cosmwasm-smart-contracts/mixnet-contract", @@ -64,6 +65,8 @@ members = [ "common/nym-id", "common/nym-metrics", "common/nym_offline_compact_ecash", + "common/nymnoise", + "common/nymnoise/keys", "common/nymsphinx", "common/nymsphinx/acknowledgements", "common/nymsphinx/addressing", @@ -131,7 +134,8 @@ members = [ "tools/internal/testnet-manager", "tools/internal/testnet-manager", "tools/internal/testnet-manager/dkg-bypass-contract", - "tools/internal/testnet-manager/dkg-bypass-contract", "tools/internal/validator-status-check", + "tools/internal/testnet-manager/dkg-bypass-contract", + "tools/internal/validator-status-check", "tools/nym-cli", "tools/nym-id-cli", "tools/nym-nr-query", @@ -305,6 +309,7 @@ serde_with = "3.9.0" serde_yaml = "0.9.25" sha2 = "0.10.9" si-scale = "0.2.3" +snow = "0.9.6" sphinx-packet = "=0.6.0" sqlx = "0.7.4" strum = "0.26" diff --git a/common/nymnoise/Cargo.toml b/common/nymnoise/Cargo.toml new file mode 100644 index 00000000000..d1a3b001f71 --- /dev/null +++ b/common/nymnoise/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "nym-noise" +version = "0.1.0" +authors = ["Simon Wicky "] +edition = "2021" +license.workspace = true + +[dependencies] +arc-swap = { workspace = true } +bytes = { workspace = true } +futures = { workspace = true } +log = { workspace = true } +pin-project = { workspace = true } +serde = { workspace = true, features = ["derive"] } +sha2 = { workspace = true } +snow = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["net", "io-util", "time"] } +tokio-util = { workspace = true, features = ["codec"] } + +# internal +nym-crypto = { path = "../crypto" } +nym-noise-keys = { path = "keys" } diff --git a/common/nymnoise/keys/Cargo.toml b/common/nymnoise/keys/Cargo.toml new file mode 100644 index 00000000000..67444cd589c --- /dev/null +++ b/common/nymnoise/keys/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "nym-noise-keys" +version = "0.1.0" +authors = ["Simon Wicky "] +edition = "2021" +license.workspace = true + +[dependencies] +schemars = { workspace = true, features = ["preserve_order"] } +serde = { workspace = true, features = ["derive"] } +utoipa = { workspace = true } + +# internal +nym-crypto = { path = "../../crypto" } diff --git a/common/nymnoise/keys/src/lib.rs b/common/nymnoise/keys/src/lib.rs new file mode 100644 index 00000000000..00a0fbfd39c --- /dev/null +++ b/common/nymnoise/keys/src/lib.rs @@ -0,0 +1,40 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_crypto::asymmetric::x25519; +use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey; +use serde::{Deserialize, Serialize}; + +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +#[serde(from = "u8", into = "u8")] +pub enum NoiseVersion { + V1 = 1, + Unknown, +} + +impl From for NoiseVersion { + fn from(value: u8) -> Self { + match value { + 1 => NoiseVersion::V1, + _ => NoiseVersion::Unknown, + } + } +} + +impl From for u8 { + fn from(version: NoiseVersion) -> Self { + version as u8 + } +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)] +pub struct VersionedNoiseKey { + #[schemars(with = "u8")] + #[schema(value_type = u8)] + pub version: NoiseVersion, + + #[schemars(with = "String")] + #[serde(with = "bs58_x25519_pubkey")] + #[schema(value_type = String)] + pub x25519_pubkey: x25519::PublicKey, +} diff --git a/common/nymnoise/src/config.rs b/common/nymnoise/src/config.rs new file mode 100644 index 00000000000..3554d25897e --- /dev/null +++ b/common/nymnoise/src/config.rs @@ -0,0 +1,132 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use std::{ + collections::HashMap, + net::{IpAddr, SocketAddr}, + sync::Arc, +}; + +use arc_swap::ArcSwap; +use nym_crypto::asymmetric::x25519; +use nym_noise_keys::{NoiseVersion, VersionedNoiseKey}; + +#[derive(Default, Debug, Clone, Copy)] +pub enum NoisePattern { + #[default] + XKpsk3, + IKpsk2, +} + +impl NoisePattern { + pub(crate) fn as_str(&self) -> &'static str { + match self { + Self::XKpsk3 => "Noise_XKpsk3_25519_AESGCM_SHA256", + Self::IKpsk2 => "Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s", //Wireguard handshake (not exactly though) + } + } + + pub(crate) fn psk_position(&self) -> u8 { + //automatic parsing, works for correct pattern, more convenient + match self.as_str().find("psk") { + Some(n) => { + let psk_index = n + 3; + let psk_char = self.as_str().chars().nth(psk_index).unwrap(); + psk_char.to_string().parse().unwrap() + //if this fails, it means hardcoded pattern are wrong + } + None => 0, + } + } +} + +#[derive(Debug, Default)] +struct SocketAddrToKey { + inner: ArcSwap>, +} + +// SW NOTE : Only for phased upgrade. To remove once we decide all nodes have to support Noise +#[derive(Debug, Default)] +struct IpAddrToVersion { + inner: ArcSwap>, +} + +#[derive(Debug, Clone, Default)] +pub struct NoiseNetworkView { + keys: Arc, + support: Arc, +} + +impl NoiseNetworkView { + pub fn new_empty() -> Self { + NoiseNetworkView { + keys: Default::default(), + support: Default::default(), + } + } + + pub fn swap_view(&self, new: HashMap) { + let noise_support = new + .iter() + .map(|(s_addr, key)| (s_addr.ip(), key.version)) + .collect::>(); + self.keys.inner.store(Arc::new(new)); + self.support.inner.store(Arc::new(noise_support)); + } +} + +#[derive(Clone)] +pub struct NoiseConfig { + network: NoiseNetworkView, + + pub(crate) local_key: Arc, + pub(crate) pattern: NoisePattern, + + pub(crate) unsafe_disabled: bool, // allows for nodes to not attempt to do a noise handshake, VERY UNSAFE, FOR DEBUG PURPOSE ONLY +} + +impl NoiseConfig { + pub fn new(noise_key: Arc, network: NoiseNetworkView) -> Self { + NoiseConfig { + network, + local_key: noise_key, + pattern: Default::default(), + unsafe_disabled: false, + } + } + + #[must_use] + pub fn with_noise_pattern(mut self, pattern: NoisePattern) -> Self { + self.pattern = pattern; + self + } + + #[must_use] + pub fn with_unsafe_disabled(mut self, disabled: bool) -> Self { + self.unsafe_disabled = disabled; + self + } + + pub(crate) fn get_noise_key(&self, s_address: &SocketAddr) -> Option { + self.network.keys.inner.load().get(s_address).copied() + } + + // Only for phased update + //SW This can lead to some troubles if two nodes shares the same IP and one support Noise but not the other. This in only for the progressive update though and there is no workaround + pub(crate) fn get_noise_support(&self, ip_addr: IpAddr) -> Option { + self.network + .support + .inner + .load() + .get(&ip_addr) + .copied() + .or_else(|| { + self.network + .support + .inner + .load() + .get(&ip_addr.to_canonical()) // SW default bind address being [::]:1789, it can happen that a responder sees the ipv6-mapped address of the initiator, this check for that + .copied() + }) + } +} diff --git a/common/nymnoise/src/connection.rs b/common/nymnoise/src/connection.rs new file mode 100644 index 00000000000..1c125df7a09 --- /dev/null +++ b/common/nymnoise/src/connection.rs @@ -0,0 +1,72 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use std::io; + +use pin_project::pin_project; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + net::TcpStream, +}; + +use crate::stream::NoiseStream; + +#[pin_project(project = ConnectionProj)] +pub enum Connection { + Tcp(#[pin] TcpStream), + Noise(#[pin] NoiseStream), +} + +impl Connection { + pub fn peer_addr(&self) -> Result { + match self { + Self::Noise(stream) => stream.peer_addr(), + Self::Tcp(stream) => stream.peer_addr(), + } + } +} + +impl AsyncRead for Connection { + fn poll_read( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + match self.project() { + ConnectionProj::Noise(stream) => stream.poll_read(cx, buf), + ConnectionProj::Tcp(stream) => stream.poll_read(cx, buf), + } + } +} + +impl AsyncWrite for Connection { + fn poll_write( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + match self.project() { + ConnectionProj::Noise(stream) => stream.poll_write(cx, buf), + ConnectionProj::Tcp(stream) => stream.poll_write(cx, buf), + } + } + fn poll_flush( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match self.project() { + ConnectionProj::Noise(stream) => stream.poll_flush(cx), + ConnectionProj::Tcp(stream) => stream.poll_flush(cx), + } + } + + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match self.project() { + ConnectionProj::Noise(stream) => stream.poll_shutdown(cx), + ConnectionProj::Tcp(stream) => stream.poll_shutdown(cx), + } + } +} diff --git a/common/nymnoise/src/error.rs b/common/nymnoise/src/error.rs new file mode 100644 index 00000000000..12d0ce21858 --- /dev/null +++ b/common/nymnoise/src/error.rs @@ -0,0 +1,36 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use snow::Error; +use std::io; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum NoiseError { + #[error("encountered a Noise decryption error")] + DecryptionError, + + #[error("encountered a Noise Protocol error - {0}")] + ProtocolError(Error), + + #[error("encountered an IO error - {0}")] + IoError(#[from] io::Error), + + #[error("Incorrect state")] + IncorrectStateError, + + #[error("Handshake did not complete")] + HandshakeError, + + #[error("Unknown noise version")] + UnknownVersion, +} + +impl From for NoiseError { + fn from(err: Error) -> Self { + match err { + Error::Decrypt => NoiseError::DecryptionError, + err => NoiseError::ProtocolError(err), + } + } +} diff --git a/common/nymnoise/src/lib.rs b/common/nymnoise/src/lib.rs new file mode 100644 index 00000000000..857cec53fee --- /dev/null +++ b/common/nymnoise/src/lib.rs @@ -0,0 +1,158 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::{NoiseConfig, NoisePattern}; +use crate::connection::Connection; +use crate::error::NoiseError; +use crate::stream::NoiseStream; +use log::*; +use nym_crypto::asymmetric::x25519; +use nym_noise_keys::NoiseVersion; +use sha2::{Digest, Sha256}; +use snow::{error::Prerequisite, Builder, Error}; +use tokio::net::TcpStream; + +pub mod config; +pub mod connection; +pub mod error; +pub mod stream; + +const NOISE_PSK_PREFIX: &[u8] = b"NYMTECH_NOISE_dQw4w9WgXcQ"; + +pub const NOISE_VERSION: NoiseVersion = NoiseVersion::V1; + +async fn upgrade_noise_initiator_v1( + conn: TcpStream, + pattern: NoisePattern, + local_private_key: &x25519::PrivateKey, + remote_pub_key: &x25519::PublicKey, +) -> Result { + trace!("Perform Noise Handshake, initiator side"); + + let secret = [ + NOISE_PSK_PREFIX.to_vec(), + remote_pub_key.to_bytes().to_vec(), + ] + .concat(); + let secret_hash = Sha256::digest(secret); + + let handshake = Builder::new(pattern.as_str().parse()?) + .local_private_key(&local_private_key.to_bytes()) + .remote_public_key(&remote_pub_key.to_bytes()) + .psk(pattern.psk_position(), &secret_hash) + .build_initiator()?; + + let noise_stream = NoiseStream::new(conn, handshake); + + Ok(Connection::Noise(noise_stream.perform_handshake().await?)) +} + +pub async fn upgrade_noise_initiator( + conn: TcpStream, + config: &NoiseConfig, +) -> Result { + if config.unsafe_disabled { + warn!("Noise is disabled in the config. Not attempting any handshake"); + return Ok(Connection::Tcp(conn)); + } + + //Get init material + let responder_addr = conn.peer_addr().map_err(|err| { + error!("Unable to extract peer address from connection - {err}"); + Error::Prereq(Prerequisite::RemotePublicKey) + })?; + + match config.get_noise_key(&responder_addr) { + Some(key) => match key.version { + NoiseVersion::V1 => { + upgrade_noise_initiator_v1( + conn, + config.pattern, + config.local_key.private_key(), + &key.x25519_pubkey, + ) + .await + } + NoiseVersion::Unknown => { + error!( + "{:?} is announcing an unknown version of Noise", + responder_addr + ); + Err(NoiseError::UnknownVersion) + } + }, + None => { + warn!( + "{:?} can't speak Noise yet, falling back to TCP", + responder_addr + ); + Ok(Connection::Tcp(conn)) + } + } +} + +async fn upgrade_noise_responder_v1( + conn: TcpStream, + pattern: NoisePattern, + local_public_key: &x25519::PublicKey, + local_private_key: &x25519::PrivateKey, +) -> Result { + trace!("Perform Noise Handshake, responder side"); + + let secret = [ + NOISE_PSK_PREFIX.to_vec(), + local_public_key.to_bytes().to_vec(), + ] + .concat(); + let secret_hash = Sha256::digest(secret); + + let handshake = Builder::new(pattern.as_str().parse()?) + .local_private_key(&local_private_key.to_bytes()) + .psk(pattern.psk_position(), &secret_hash) + .build_responder()?; + + let noise_stream = NoiseStream::new(conn, handshake); + + Ok(Connection::Noise(noise_stream.perform_handshake().await?)) +} + +pub async fn upgrade_noise_responder( + conn: TcpStream, + config: &NoiseConfig, +) -> Result { + if config.unsafe_disabled { + warn!("Noise is disabled in the config. Not attempting any handshake"); + return Ok(Connection::Tcp(conn)); + } + + //Get init material + let initiator_addr = match conn.peer_addr() { + Ok(addr) => addr, + Err(err) => { + error!("Unable to extract peer address from connection - {err}"); + return Err(Error::Prereq(Prerequisite::RemotePublicKey).into()); + } + }; + + // Port is random and we just need the support info + match config.get_noise_support(initiator_addr.ip()) { + None => { + warn!( + "{:?} can't speak Noise yet, falling back to TCP", + initiator_addr + ); + Ok(Connection::Tcp(conn)) + } + //responder's info on version is shaky, so initiator has to adapt. This behavior can change in the future + Some(_) => { + //Existing node supporting Noise + upgrade_noise_responder_v1( + conn, + config.pattern, + config.local_key.public_key(), + config.local_key.private_key(), + ) + .await + } + } +} diff --git a/common/nymnoise/src/stream.rs b/common/nymnoise/src/stream.rs new file mode 100644 index 00000000000..ef9e470cb8d --- /dev/null +++ b/common/nymnoise/src/stream.rs @@ -0,0 +1,191 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NoiseError; +use bytes::BytesMut; +use futures::{Sink, SinkExt, Stream, StreamExt}; +use pin_project::pin_project; +use snow::{HandshakeState, TransportState}; +use std::cmp::min; +use std::collections::VecDeque; +use std::io; +use std::pin::Pin; +use std::task::Poll; +use tokio::{ + io::{AsyncRead, AsyncWrite, ReadBuf}, + net::TcpStream, +}; +use tokio_util::codec::{Framed, LengthDelimitedCodec}; + +const MAXMSGLEN: usize = 65535; +const TAGLEN: usize = 16; + +/// Wrapper around a TcpStream +#[pin_project] +pub struct NoiseStream { + #[pin] + inner_stream: Framed, + handshake: Option, + noise: Option, + dec_buffer: VecDeque, +} + +impl NoiseStream { + pub(crate) fn new(inner_stream: TcpStream, handshake: HandshakeState) -> NoiseStream { + NoiseStream { + inner_stream: LengthDelimitedCodec::builder() + .length_field_type::() + .new_framed(inner_stream), + handshake: Some(handshake), + noise: None, + dec_buffer: VecDeque::with_capacity(MAXMSGLEN), + } + } + + pub(crate) async fn perform_handshake(mut self) -> Result { + //Check if we are in the correct state + let Some(mut handshake) = self.handshake else { + return Err(NoiseError::IncorrectStateError); + }; + self.handshake = None; + + while !handshake.is_handshake_finished() { + if handshake.is_my_turn() { + self.send_handshake_msg(&mut handshake).await?; + } else { + self.recv_handshake_msg(&mut handshake).await?; + } + } + + self.noise = Some(handshake.into_transport_mode()?); + Ok(self) + } + + async fn send_handshake_msg( + &mut self, + handshake: &mut HandshakeState, + ) -> Result<(), NoiseError> { + let mut buf = BytesMut::zeroed(MAXMSGLEN + TAGLEN); + let len = handshake.write_message(&[], &mut buf)?; + buf.truncate(len); + self.inner_stream.send(buf.into()).await?; + Ok(()) + } + + async fn recv_handshake_msg( + &mut self, + handshake: &mut HandshakeState, + ) -> Result<(), NoiseError> { + match self.inner_stream.next().await { + Some(Ok(msg)) => { + let mut buf = vec![0u8; MAXMSGLEN]; + handshake.read_message(&msg, &mut buf)?; + Ok(()) + } + Some(Err(err)) => Err(NoiseError::IoError(err)), + None => Err(NoiseError::HandshakeError), + } + } + + pub fn peer_addr(&self) -> Result { + self.inner_stream.get_ref().peer_addr() + } +} + +impl AsyncRead for NoiseStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let projected_self = self.project(); + + match projected_self.inner_stream.poll_next(cx) { + Poll::Pending => { + //no new data, waking is already scheduled. + //Nothing new to decrypt, only check if we can return something from dec_storage, happens after + } + + Poll::Ready(Some(Ok(noise_msg))) => { + //We have a new moise msg + let mut dec_msg = vec![0u8; MAXMSGLEN]; + let len = match projected_self.noise { + Some(transport_state) => { + match transport_state.read_message(&noise_msg, &mut dec_msg) { + Ok(len) => len, + Err(_) => return Poll::Ready(Err(io::ErrorKind::InvalidInput.into())), + } + } + None => return Poll::Ready(Err(io::ErrorKind::Other.into())), + }; + projected_self.dec_buffer.extend(&dec_msg[..len]); + } + + Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(err)), + + //Stream is done, return Ok with nothing in buf + Poll::Ready(None) => return Poll::Ready(Ok(())), + } + + //check and return what we can + let read_len = min(buf.remaining(), projected_self.dec_buffer.len()); + if read_len > 0 { + buf.put_slice( + &projected_self + .dec_buffer + .drain(..read_len) + .collect::>(), + ); + return Poll::Ready(Ok(())); + } + + //If we end up here, it must mean the previous poll_next was pending as well, otherwise something was returned. Hence waking is already scheduled + Poll::Pending + } +} + +impl AsyncWrite for NoiseStream { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> Poll> { + let mut projected_self = self.project(); + + match projected_self.inner_stream.as_mut().poll_ready(cx) { + Poll::Pending => Poll::Pending, + + Poll::Ready(Err(err)) => Poll::Ready(Err(err)), + + Poll::Ready(Ok(())) => { + let mut noise_buf = BytesMut::zeroed(MAXMSGLEN + TAGLEN); + + let Ok(len) = (match projected_self.noise { + Some(transport_state) => transport_state.write_message(buf, &mut noise_buf), + None => return Poll::Ready(Err(io::ErrorKind::Other.into())), + }) else { + return Poll::Ready(Err(io::ErrorKind::InvalidInput.into())); + }; + noise_buf.truncate(len); + match projected_self.inner_stream.start_send(noise_buf.into()) { + Ok(()) => Poll::Ready(Ok(buf.len())), + Err(e) => Poll::Ready(Err(e)), + } + } + } + } + + fn poll_flush( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + self.project().inner_stream.poll_flush(cx) + } + + fn poll_shutdown( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + self.project().inner_stream.poll_close(cx) + } +} From 6cf8d7998859cdcd0d3007ce2dd49d20c7fb9be3 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Tue, 20 May 2025 16:03:47 +0200 Subject: [PATCH 02/21] adding proper noise key announcing on nodes --- nym-node/Cargo.toml | 2 ++ nym-node/nym-node-requests/Cargo.toml | 1 + nym-node/nym-node-requests/src/api/mod.rs | 14 +++++++++++--- .../nym-node-requests/src/api/v1/node/models.rs | 13 ++++--------- nym-node/src/node/http/helpers/mod.rs | 3 ++- nym-node/src/node/mod.rs | 7 ++++++- 6 files changed, 26 insertions(+), 14 deletions(-) diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 72eb5924eb0..1972823d707 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -55,6 +55,8 @@ nym-config = { path = "../common/config" } nym-crypto = { path = "../common/crypto", features = ["asymmetric", "rand"] } nym-nonexhaustive-delayqueue = { path = "../common/nonexhaustive-delayqueue" } nym-mixnet-client = { path = "../common/client-libs/mixnet-client" } +nym-noise = { path = "../common/nymnoise" } +nym-noise-keys = { path = "../common/nymnoise/keys" } nym-pemstore = { path = "../common/pemstore" } nym-sphinx-acknowledgements = { path = "../common/nymsphinx/acknowledgements" } nym-sphinx-addressing = { path = "../common/nymsphinx/addressing" } diff --git a/nym-node/nym-node-requests/Cargo.toml b/nym-node/nym-node-requests/Cargo.toml index 9e889439305..0d1628f34b0 100644 --- a/nym-node/nym-node-requests/Cargo.toml +++ b/nym-node/nym-node-requests/Cargo.toml @@ -26,6 +26,7 @@ nym-crypto = { path = "../../common/crypto", features = [ "serde", ] } nym-exit-policy = { path = "../../common/exit-policy" } +nym-noise-keys = { path = "../../common/nymnoise/keys" } nym-wireguard-types = { path = "../../common/wireguard-types", default-features = false } # feature-specific dependencies: diff --git a/nym-node/nym-node-requests/src/api/mod.rs b/nym-node/nym-node-requests/src/api/mod.rs index c2ae5badb4c..49132ea6e27 100644 --- a/nym-node/nym-node-requests/src/api/mod.rs +++ b/nym-node/nym-node-requests/src/api/mod.rs @@ -105,8 +105,10 @@ impl Display for ErrorResponse { #[cfg(test)] mod tests { + use super::*; use nym_crypto::asymmetric::{ed25519, x25519}; + use nym_noise_keys::{NoiseVersion, VersionedNoiseKey}; use rand_chacha::rand_core::SeedableRng; #[test] @@ -114,7 +116,10 @@ mod tests { let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); let ed22519 = ed25519::KeyPair::new(&mut rng); let x25519_sphinx = x25519::KeyPair::new(&mut rng); - let x25519_noise = x25519::KeyPair::new(&mut rng); + let x25519_noise = VersionedNoiseKey { + version: NoiseVersion::V1, + x25519_pubkey: *x25519::KeyPair::new(&mut rng).public_key(), + }; let host_info = crate::api::v1::node::models::HostInformation { ip_address: vec!["1.1.1.1".parse().unwrap()], @@ -136,7 +141,7 @@ mod tests { keys: crate::api::v1::node::models::HostKeys { ed25519_identity: *ed22519.public_key(), x25519_sphinx: *x25519_sphinx.public_key(), - x25519_noise: Some(*x25519_noise.public_key()), + x25519_noise: Some(x25519_noise), }, }; @@ -189,7 +194,10 @@ mod tests { keys: crate::api::v1::node::models::HostKeys { ed25519_identity: legacy_info_noise.keys.ed25519_identity.parse().unwrap(), x25519_sphinx: legacy_info_noise.keys.x25519_sphinx.parse().unwrap(), - x25519_noise: Some(legacy_info_noise.keys.x25519_noise.parse().unwrap()), + x25519_noise: Some(VersionedNoiseKey { + version: NoiseVersion::V1, + x25519_pubkey: legacy_info_noise.keys.x25519_noise.parse().unwrap(), + }), }, }; diff --git a/nym-node/nym-node-requests/src/api/v1/node/models.rs b/nym-node/nym-node-requests/src/api/v1/node/models.rs index 5f047912faa..04655e3f886 100644 --- a/nym-node/nym-node-requests/src/api/v1/node/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/node/models.rs @@ -3,10 +3,8 @@ use celes::Country; use nym_crypto::asymmetric::ed25519::{self, serde_helpers::bs58_ed25519_pubkey}; -use nym_crypto::asymmetric::x25519::{ - self, - serde_helpers::{bs58_x25519_pubkey, option_bs58_x25519_pubkey}, -}; +use nym_crypto::asymmetric::x25519::{self, serde_helpers::bs58_x25519_pubkey}; +use nym_noise_keys::VersionedNoiseKey; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::net::IpAddr; @@ -124,10 +122,7 @@ pub struct HostKeys { /// Base58-encoded x25519 public key of this node used for the noise protocol. #[serde(default)] - #[serde(with = "option_bs58_x25519_pubkey")] - #[schemars(with = "Option")] - #[cfg_attr(feature = "openapi", schema(value_type = Option))] - pub x25519_noise: Option, + pub x25519_noise: Option, } #[derive(Serialize)] @@ -150,7 +145,7 @@ impl From for LegacyHostKeysV2 { x25519_sphinx: value.x25519_sphinx.to_base58_string(), x25519_noise: value .x25519_noise - .map(|k| k.to_base58_string()) + .map(|k| k.x25519_pubkey.to_base58_string()) .unwrap_or_default(), } } diff --git a/nym-node/src/node/http/helpers/mod.rs b/nym-node/src/node/http/helpers/mod.rs index a819bdd062d..04114832e1a 100644 --- a/nym-node/src/node/http/helpers/mod.rs +++ b/nym-node/src/node/http/helpers/mod.rs @@ -7,13 +7,14 @@ use crate::node::http::api::api_requests; use crate::node::http::error::NymNodeHttpError; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_node_requests::api::SignedHostInformation; +use nym_noise_keys::VersionedNoiseKey; pub mod system_info; pub(crate) fn sign_host_details( config: &Config, x22519_sphinx: &x25519::PublicKey, - x25519_noise: &x25519::PublicKey, + x25519_noise: &VersionedNoiseKey, ed22519_identity: &ed25519::KeyPair, ) -> Result { let x25519_noise = if config.mixnet.debug.unsafe_disable_noise { diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index f3b3b3b1b92..63a1a41b072 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -44,6 +44,8 @@ use nym_network_requester::{ use nym_node_metrics::events::MetricEventsSender; use nym_node_metrics::NymNodeMetrics; use nym_node_requests::api::v1::node::models::{AnnouncePorts, NodeDescription}; +use nym_noise::config::{NoiseConfig, NoiseNetworkView}; +use nym_noise_keys::VersionedNoiseKey; use nym_sphinx_acknowledgements::AckKey; use nym_sphinx_addressing::Recipient; use nym_task::{ShutdownManager, ShutdownToken, TaskClient}; @@ -700,7 +702,10 @@ impl NymNode { let host_details = sign_host_details( &self.config, self.x25519_sphinx_keys.public_key(), - self.x25519_noise_keys.public_key(), + &VersionedNoiseKey { + version: nym_noise::NOISE_VERSION, + x25519_pubkey: *self.x25519_noise_keys.public_key(), + }, &self.ed25519_identity_keys, )?; From eabad283fa887b304a457092fc7d1cc3295cbadf Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Tue, 20 May 2025 16:07:20 +0200 Subject: [PATCH 03/21] add semi-skimmed endpoint to distribute noise key --- Cargo.lock | 8 +- .../validator-client/src/client.rs | 27 +++- .../validator-client/src/nym_api/mod.rs | 34 +++++ nym-api/nym-api-requests/Cargo.toml | 1 + nym-api/nym-api-requests/src/models.rs | 52 ++++++-- nym-api/nym-api-requests/src/nym_nodes.rs | 4 +- nym-api/src/nym_nodes/handlers/mod.rs | 2 +- .../nym_nodes/handlers/unstable/helpers.rs | 21 ++- .../handlers/unstable/semi_skimmed.rs | 120 +++++++++++++++++- 9 files changed, 244 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0838876774f..f0775282248 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4888,6 +4888,7 @@ dependencies = [ "nym-mixnet-contract-common", "nym-network-defaults", "nym-node-requests", + "nym-noise-keys", "nym-serde-helpers", "nym-ticketbooks-merkle", "rand_chacha 0.3.1", @@ -6213,6 +6214,8 @@ dependencies = [ "nym-network-requester", "nym-node-metrics", "nym-node-requests", + "nym-noise", + "nym-noise-keys", "nym-nonexhaustive-delayqueue", "nym-pemstore", "nym-sphinx-acknowledgements", @@ -6276,6 +6279,7 @@ dependencies = [ "nym-crypto", "nym-exit-policy", "nym-http-api-client", + "nym-noise-keys", "nym-wireguard-types", "rand_chacha 0.3.1", "schemars", @@ -6423,7 +6427,7 @@ dependencies = [ "nym-noise-keys", "pin-project", "serde", - "sha2 0.10.8", + "sha2 0.10.9", "snow", "thiserror 2.0.12", "tokio", @@ -9317,7 +9321,7 @@ dependencies = [ "curve25519-dalek", "rand_core 0.6.4", "rustc_version 0.4.1", - "sha2 0.10.8", + "sha2 0.10.9", "subtle 2.6.1", ] diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index ace9e396e61..c078317bab6 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -25,7 +25,7 @@ use nym_api_requests::models::{ NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse, }; use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated}; -use nym_api_requests::nym_nodes::{NodesByAddressesResponse, SkimmedNode}; +use nym_api_requests::nym_nodes::{NodesByAddressesResponse, SemiSkimmedNode, SkimmedNode}; use nym_coconut_dkg_common::types::EpochId; use nym_http_api_client::UserAgent; use nym_mixnet_contract_common::EpochRewardedSet; @@ -524,6 +524,31 @@ impl NymApiClient { Ok(nodes) } + /// retrieve expanded information for all bonded nodes on the network + pub async fn get_all_expanded_nodes( + &self, + ) -> Result, ValidatorClientError> { + // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere + let mut page = 0; + let mut nodes = Vec::new(); + + loop { + let mut res = self + .nym_api + .get_expanded_nodes(false, Some(page), None) + .await?; + + nodes.append(&mut res.nodes.data); + if nodes.len() < res.nodes.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(nodes) + } + pub async fn health(&self) -> Result { Ok(self.nym_api.health().await?) } diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index 6922e53dd45..41682242a6e 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -19,6 +19,7 @@ use nym_api_requests::models::{ }; use nym_api_requests::nym_nodes::{ NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponse, + SemiSkimmedNode, }; use nym_api_requests::pagination::PaginatedResponse; pub use nym_api_requests::{ @@ -474,6 +475,39 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] + async fn get_expanded_nodes( + &self, + no_legacy: bool, + page: Option, + per_page: Option, + ) -> Result, NymAPIError> { + let mut params = Vec::new(); + + if no_legacy { + params.push(("no_legacy", "true".to_string())) + } + + if let Some(page) = page { + params.push(("page", page.to_string())) + } + + if let Some(per_page) = per_page { + params.push(("per_page", per_page.to_string())) + } + + self.get_json( + &[ + routes::API_VERSION, + "unstable", + routes::NYM_NODES_ROUTES, + "semi-skimmed", + ], + ¶ms, + ) + .await + } + #[deprecated] #[instrument(level = "debug", skip(self))] async fn get_active_mixnodes(&self) -> Result, NymAPIError> { diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index e75f21b0bad..c695e5b3f49 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -37,6 +37,7 @@ nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", features = ["naive_float"] } nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"] } nym-node-requests = { path = "../../nym-node/nym-node-requests", default-features = false, features = ["openapi"] } +nym-noise-keys = { path = "../../common/nymnoise/keys"} nym-network-defaults = { path = "../../common/network-defaults" } nym-ticketbooks-merkle = { path = "../../common/ticketbooks-merkle" } diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index a39b6f25c78..8109b23f45d 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -8,15 +8,13 @@ use crate::helpers::PlaceholderJsonSchemaImpl; use crate::legacy::{ LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, }; +use crate::nym_nodes::SemiSkimmedNode; use crate::nym_nodes::{BasicEntryInformation, NodeRole, SkimmedNode}; use crate::pagination::PaginatedResponse; use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; use nym_contracts_common::NaiveFloat; use nym_crypto::asymmetric::ed25519::{self, serde_helpers::bs58_ed25519_pubkey}; -use nym_crypto::asymmetric::x25519::{ - self, - serde_helpers::{bs58_x25519_pubkey, option_bs58_x25519_pubkey}, -}; +use nym_crypto::asymmetric::x25519::{self, serde_helpers::bs58_x25519_pubkey}; use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::reward_params::{Performance, RewardingParams}; use nym_mixnet_contract_common::rewarding::RewardEstimate; @@ -28,6 +26,7 @@ use nym_node_requests::api::v1::authenticator::models::Authenticator; use nym_node_requests::api::v1::gateway::models::Wireguard; use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter; use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, NodeRoles}; +use nym_noise_keys::VersionedNoiseKey; use schemars::gen::SchemaGenerator; use schemars::schema::{InstanceType, Schema, SchemaObject}; use schemars::JsonSchema; @@ -465,6 +464,17 @@ impl MixNodeBondAnnotated { performance: self.node_performance.last_24h, }) } + + pub fn try_to_semi_skimmed_node( + &self, + role: NodeRole, + ) -> Result { + let skimmed_node = self.try_to_skimmed_node(role)?; + Ok(SemiSkimmedNode { + basic: skimmed_node, + x25519_noise_versioned_key: None, // legacy node won't ever support Noise + }) + } } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] @@ -530,6 +540,17 @@ impl GatewayBondAnnotated { performance: self.node_performance.last_24h, }) } + + pub fn try_to_semi_skimmed_node( + &self, + role: NodeRole, + ) -> Result { + let skimmed_node = self.try_to_skimmed_node(role)?; + Ok(SemiSkimmedNode { + basic: skimmed_node, + x25519_noise_versioned_key: None, // legacy node won't ever support Noise + }) + } } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] @@ -866,10 +887,7 @@ pub struct HostKeys { pub x25519: x25519::PublicKey, #[serde(default)] - #[serde(with = "option_bs58_x25519_pubkey")] - #[schemars(with = "Option")] - #[schema(value_type = String)] - pub x25519_noise: Option, + pub x25519_noise: Option, } impl From for HostKeys { @@ -1022,6 +1040,19 @@ impl NymNodeDescription { performance, } } + + pub fn to_semi_skimmed_node( + &self, + role: NodeRole, + performance: Performance, + ) -> SemiSkimmedNode { + let skimmed_node = self.to_skimmed_node(role, performance); + + SemiSkimmedNode { + basic: skimmed_node, + x25519_noise_versioned_key: self.description.host_information.keys.x25519_noise, + } + } } #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] @@ -1307,10 +1338,7 @@ pub struct NetworkMonitorRunDetailsResponse { #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct NoiseDetails { - #[schemars(with = "String")] - #[serde(with = "bs58_x25519_pubkey")] - #[schema(value_type = String)] - pub x25119_pubkey: x25519::PublicKey, + pub key: VersionedNoiseKey, pub mixnet_port: u16, diff --git a/nym-api/nym-api-requests/src/nym_nodes.rs b/nym-api/nym-api-requests/src/nym_nodes.rs index 544e07a8617..21633d84c51 100644 --- a/nym-api/nym-api-requests/src/nym_nodes.rs +++ b/nym-api/nym-api-requests/src/nym_nodes.rs @@ -9,6 +9,7 @@ use nym_crypto::asymmetric::{ed25519, x25519}; use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::{Interval, NodeId}; +use nym_noise_keys::VersionedNoiseKey; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::net::IpAddr; @@ -202,7 +203,8 @@ impl SkimmedNode { #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct SemiSkimmedNode { pub basic: SkimmedNode, - pub x25519_noise_pubkey: String, + + pub x25519_noise_versioned_key: Option, // pub location: } diff --git a/nym-api/src/nym_nodes/handlers/mod.rs b/nym-api/src/nym_nodes/handlers/mod.rs index bd9dd674d60..1420e490011 100644 --- a/nym-api/src/nym_nodes/handlers/mod.rs +++ b/nym-api/src/nym_nodes/handlers/mod.rs @@ -174,7 +174,7 @@ async fn nodes_noise( .map(|noise_key| (noise_key, n)) }) .map(|(noise_key, node)| NoiseDetails { - x25119_pubkey: noise_key, + key: noise_key, mixnet_port: node.description.mix_port(), ip_addresses: node.description.host_information.ip_address.clone(), }) diff --git a/nym-api/src/nym_nodes/handlers/unstable/helpers.rs b/nym-api/src/nym_nodes/handlers/unstable/helpers.rs index 3940d992379..189f798209e 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/helpers.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/helpers.rs @@ -4,7 +4,7 @@ use nym_api_requests::models::{ GatewayBondAnnotated, MalformedNodeBond, MixNodeBondAnnotated, OffsetDateTimeJsonSchemaWrapper, }; -use nym_api_requests::nym_nodes::{NodeRole, SkimmedNode}; +use nym_api_requests::nym_nodes::{NodeRole, SemiSkimmedNode, SkimmedNode}; use nym_mixnet_contract_common::reward_params::Performance; use time::OffsetDateTime; @@ -14,6 +14,11 @@ pub(crate) trait LegacyAnnotation { fn identity(&self) -> &str; fn try_to_skimmed_node(&self, role: NodeRole) -> Result; + + fn try_to_semi_skimmed_node( + &self, + role: NodeRole, + ) -> Result; } impl LegacyAnnotation for MixNodeBondAnnotated { @@ -28,6 +33,13 @@ impl LegacyAnnotation for MixNodeBondAnnotated { fn try_to_skimmed_node(&self, role: NodeRole) -> Result { self.try_to_skimmed_node(role) } + + fn try_to_semi_skimmed_node( + &self, + role: NodeRole, + ) -> Result { + self.try_to_semi_skimmed_node(role) + } } impl LegacyAnnotation for GatewayBondAnnotated { @@ -42,6 +54,13 @@ impl LegacyAnnotation for GatewayBondAnnotated { fn try_to_skimmed_node(&self, role: NodeRole) -> Result { self.try_to_skimmed_node(role) } + + fn try_to_semi_skimmed_node( + &self, + role: NodeRole, + ) -> Result { + self.try_to_semi_skimmed_node(role) + } } pub(crate) fn refreshed_at( diff --git a/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs b/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs index f7dff11e3e5..8b74396411a 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs @@ -1,13 +1,91 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::node_describe_cache::DescribedNodes; +use crate::node_status_api::models::AxumResult; +use crate::nym_nodes::handlers::unstable::helpers::{refreshed_at, LegacyAnnotation}; use crate::nym_nodes::handlers::unstable::NodesParamsWithRole; use crate::support::http::state::AppState; use axum::extract::{Query, State}; -use nym_api_requests::nym_nodes::{CachedNodesResponse, SemiSkimmedNode}; +use axum::Json; +use nym_api_requests::models::{ + NodeAnnotation, NymNodeDescription, OffsetDateTimeJsonSchemaWrapper, +}; +use nym_api_requests::nym_nodes::{NodeRole, PaginatedCachedNodesResponse, SemiSkimmedNode}; +use nym_api_requests::pagination::PaginatedResponse; use nym_http_api_common::FormattedResponse; +use nym_mixnet_contract_common::NodeId; +use nym_topology::CachedEpochRewardedSet; +use std::collections::HashMap; +use tracing::trace; +use utoipa::ToSchema; +pub type PaginatedSemiSkimmedNodes = + AxumResult>>; + +//SW TODO : this is copied from skimmed nodes, surely we can do better than that +fn build_nym_nodes_response<'a, NI>( + rewarded_set: &CachedEpochRewardedSet, + nym_nodes_subset: NI, + annotations: &HashMap, +) -> Vec +where + NI: Iterator + 'a, +{ + let mut nodes = Vec::new(); + for nym_node in nym_nodes_subset { + let node_id = nym_node.node_id; + + let role: NodeRole = rewarded_set.role(node_id).into(); + + // honestly, not sure under what exact circumstances this value could be missing, + // but in that case just use 0 performance + let annotation = annotations.get(&node_id).copied().unwrap_or_default(); + + nodes.push(nym_node.to_semi_skimmed_node(role, annotation.last_24h_performance)); + } + nodes +} + +//SW TODO : this is copied from skimmed nodes, surely we can do better than that +/// Given all relevant caches, add appropriate legacy nodes to the part of the response +fn add_legacy( + nodes: &mut Vec, + rewarded_set: &CachedEpochRewardedSet, + describe_cache: &DescribedNodes, + annotated_legacy_nodes: &HashMap, +) where + LN: LegacyAnnotation, +{ + for (node_id, legacy) in annotated_legacy_nodes.iter() { + let role: NodeRole = rewarded_set.role(*node_id).into(); + + // if we have self-described info, prefer it over contract data + if let Some(described) = describe_cache.get_node(node_id) { + nodes.push(described.to_semi_skimmed_node(role, legacy.performance())) + } else { + match legacy.try_to_semi_skimmed_node(role) { + Ok(node) => nodes.push(node), + Err(err) => { + let id = legacy.identity(); + trace!("node {id} is malformed: {err}") + } + } + } + } +} + +#[allow(dead_code)] // not dead, used in OpenAPI docs +#[derive(ToSchema)] +#[schema(title = "PaginatedCachedNodesExpandedResponseSchema")] +pub struct PaginatedCachedNodesExpandedResponseSchema { + pub refreshed_at: OffsetDateTimeJsonSchemaWrapper, + #[schema(value_type = SemiSkimmedNode)] + pub nodes: PaginatedResponse, +} + +/// Return all Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used) +/// that are currently bonded. #[utoipa::path( tag = "Unstable Nym Nodes", get, @@ -15,13 +93,41 @@ use nym_http_api_common::FormattedResponse; path = "", context_path = "/v1/unstable/nym-nodes/semi-skimmed", responses( - // (status = 200, body = CachedNodesResponse) - (status = 501) + (status = 200, body = PaginatedCachedNodesExpandedResponseSchema) ) )] pub(super) async fn nodes_expanded( - _state: State, + state: State, _query_params: Query, -) -> AxumResult>> { - Err(AxumErrorResponse::not_implemented()) +) -> PaginatedSemiSkimmedNodes { + // 1. grab all relevant described nym-nodes + let rewarded_set = state.rewarded_set().await?; + + let describe_cache = state.describe_nodes_cache_data().await?; + let all_nym_nodes = describe_cache.all_nym_nodes(); + let annotations = state.node_annotations().await?; + let legacy_mixnodes = state.legacy_mixnode_annotations().await?; + let legacy_gateways = state.legacy_gateways_annotations().await?; + + let mut nodes = build_nym_nodes_response(&rewarded_set, all_nym_nodes, &annotations); + + // add legacy gateways to the response + add_legacy(&mut nodes, &rewarded_set, &describe_cache, &legacy_gateways); + + // add legacy mixnodes to the response + add_legacy(&mut nodes, &rewarded_set, &describe_cache, &legacy_mixnodes); + + // min of all caches + let refreshed_at = refreshed_at([ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + legacy_mixnodes.timestamp(), + legacy_gateways.timestamp(), + ]); + + Ok(Json(PaginatedCachedNodesResponse::new_full( + refreshed_at, + nodes, + ))) } From ddf7c3b8605cb49f27433cebc1ecf124d53ad39e Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Tue, 20 May 2025 16:07:21 +0200 Subject: [PATCH 04/21] noise handshake common --- nym-node/src/node/mod.rs | 17 +++++++++ nym-node/src/node/shared_network.rs | 57 ++++++++++++++++++++++------- 2 files changed, 60 insertions(+), 14 deletions(-) diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index 63a1a41b072..7cfd26ea439 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -1014,6 +1014,7 @@ impl NymNode { &self, active_clients_store: &ActiveClientsStore, routing_filter: F, + noise_config: NoiseConfig, shutdown: ShutdownToken, ) -> Result<(MixForwardingSender, ActiveConnections), NymNodeError> where @@ -1037,6 +1038,7 @@ impl NymNode { ); let mixnet_client = nym_mixnet_client::Client::new( mixnet_client_config, + noise_config.clone(), self.metrics .network .active_egress_mixnet_connections_counter(), @@ -1064,6 +1066,7 @@ impl NymNode { replay_protection_bloomfilter, mix_packet_sender.clone(), final_hop_data, + noise_config, self.metrics.clone(), shutdown, ); @@ -1073,9 +1076,16 @@ impl NymNode { } pub(crate) async fn run_minimal_mixnet_processing(self) -> Result<(), NymNodeError> { + let noise_config = nym_noise::config::NoiseConfig::new( + self.x25519_noise_keys.clone(), + NoiseNetworkView::new_empty(), + ) + .with_unsafe_disabled(true); + self.start_mixnet_listener( &ActiveClientsStore::new(), OpenFilter, + noise_config, self.shutdown_manager.clone_token("mixnet-traffic"), ) .await?; @@ -1115,10 +1125,17 @@ impl NymNode { let network_refresher = self.build_network_refresher().await?; let active_clients_store = ActiveClientsStore::new(); + let noise_config = nym_noise::config::NoiseConfig::new( + self.x25519_noise_keys.clone(), + network_refresher.noise_view(), + ) + .with_unsafe_disabled(self.config.mixnet.debug.unsafe_disable_noise); + let (mix_packet_sender, active_egress_mixnet_connections) = self .start_mixnet_listener( &active_clients_store, network_refresher.routing_filter(), + noise_config, self.shutdown_manager.clone_token("mixnet-traffic"), ) .await?; diff --git a/nym-node/src/node/shared_network.rs b/nym-node/src/node/shared_network.rs index 1b6ace3210e..eb6fdf537fe 100644 --- a/nym-node/src/node/shared_network.rs +++ b/nym-node/src/node/shared_network.rs @@ -6,14 +6,15 @@ use crate::node::routing_filter::network_filter::NetworkRoutingFilter; use async_trait::async_trait; use nym_gateway::node::UserAgent; use nym_node_metrics::prometheus_wrapper::{PrometheusMetric, PROMETHEUS_METRICS}; +use nym_noise::config::NoiseNetworkView; use nym_task::ShutdownToken; use nym_topology::node::RoutingNode; use nym_topology::{EpochRewardedSet, NymTopology, Role, TopologyProvider}; use nym_validator_client::nym_api::NymApiClientExt; -use nym_validator_client::nym_nodes::{NodesByAddressesResponse, SkimmedNode}; +use nym_validator_client::nym_nodes::{NodesByAddressesResponse, SemiSkimmedNode}; use nym_validator_client::{NymApiClient, ValidatorClientError}; -use std::collections::HashSet; -use std::net::IpAddr; +use std::collections::{HashMap, HashSet}; +use std::net::{IpAddr, SocketAddr}; use std::sync::Arc; use std::time::Duration; use tokio::sync::RwLock; @@ -53,10 +54,10 @@ impl NodesQuerier { res } - async fn current_nymnodes(&mut self) -> Result, ValidatorClientError> { + async fn current_nymnodes(&mut self) -> Result, ValidatorClientError> { let res = self .client - .get_all_basic_nodes() + .get_all_expanded_nodes() .await .inspect_err(|err| error!("failed to get network nodes: {err}")); @@ -112,13 +113,19 @@ impl TopologyProvider for CachedTopologyProvider { let self_node = self.gateway_node.identity_key; let mut topology = NymTopology::new_empty(network_guard.rewarded_set.clone()) - .with_additional_nodes(network_guard.network_nodes.iter().filter(|node| { - if node.supported_roles.mixnode { - node.performance.round_to_integer() >= self.min_mix_performance - } else { - true - } - })); + .with_additional_nodes( + network_guard + .network_nodes + .iter() + .map(|node| &node.basic) + .filter(|node| { + if node.supported_roles.mixnode { + node.performance.round_to_integer() >= self.min_mix_performance + } else { + true + } + }), + ); if !topology.has_node_details(self.gateway_node.node_id) { debug!("{self_node} didn't exist in topology. inserting it.",); @@ -148,7 +155,7 @@ impl CachedNetwork { struct CachedNetworkInner { rewarded_set: EpochRewardedSet, - network_nodes: Vec, + network_nodes: Vec, } pub struct NetworkRefresher { @@ -159,6 +166,7 @@ pub struct NetworkRefresher { network: CachedNetwork, routing_filter: NetworkRoutingFilter, + noise_view: NoiseNetworkView, } impl NetworkRefresher { @@ -186,6 +194,7 @@ impl NetworkRefresher { shutdown_token, network: CachedNetwork::new_empty(), routing_filter: NetworkRoutingFilter::new_empty(testnet), + noise_view: NoiseNetworkView::new_empty(), }; this.obtain_initial_network().await?; @@ -240,7 +249,7 @@ impl NetworkRefresher { // collect all known/allowed nodes information let known_nodes = nodes .iter() - .flat_map(|n| n.ip_addresses.iter()) + .flat_map(|n| n.basic.ip_addresses.iter()) .copied() .collect::>(); @@ -263,6 +272,22 @@ impl NetworkRefresher { self.routing_filter.resolved.swap_denied(current_denied); self.routing_filter.pending.clear().await; + //update noise Noise Nodes + let noise_nodes = nodes + .iter() + .filter(|n| n.x25519_noise_versioned_key.is_some()) + .flat_map(|n| { + n.basic.ip_addresses.iter().map(|ip_addr| { + ( + SocketAddr::new(*ip_addr, n.basic.mix_port), + #[allow(clippy::unwrap_used)] + n.x25519_noise_versioned_key.unwrap(), // SAFETY : we filtered out nodes where this option can be None + ) + }) + }) + .collect::>(); + self.noise_view.swap_view(noise_nodes); + let mut network_guard = self.network.inner.write().await; network_guard.network_nodes = nodes; network_guard.rewarded_set = rewarded_set; @@ -296,6 +321,10 @@ impl NetworkRefresher { self.network.clone() } + pub(crate) fn noise_view(&self) -> NoiseNetworkView { + self.noise_view.clone() + } + pub(crate) async fn run(&mut self) { let mut full_refresh_interval = interval(self.full_refresh_interval); full_refresh_interval.reset(); From 1ee8a5a19bf02065b4caebf3033c1ab3fd5fe0a7 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Tue, 20 May 2025 16:07:21 +0200 Subject: [PATCH 05/21] noise handshake responder side --- nym-node/src/node/mixnet/handler.rs | 37 +++++++++++++++++++------- nym-node/src/node/mixnet/shared/mod.rs | 11 ++++++-- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/nym-node/src/node/mixnet/handler.rs b/nym-node/src/node/mixnet/handler.rs index 1a6cf87ad35..f6324909050 100644 --- a/nym-node/src/node/mixnet/handler.rs +++ b/nym-node/src/node/mixnet/handler.rs @@ -3,6 +3,8 @@ use crate::node::mixnet::shared::SharedData; use futures::StreamExt; +use nym_noise::connection::Connection; +use nym_noise::upgrade_noise_responder; use nym_sphinx_forwarding::packet::MixPacket; use nym_sphinx_framing::codec::NymCodec; use nym_sphinx_framing::packet::FramedNymPacket; @@ -61,7 +63,6 @@ impl PendingReplayCheckPackets { pub(crate) struct ConnectionHandler { shared: SharedData, - mixnet_connection: Framed, remote_address: SocketAddr, // packets pending for replay detection @@ -78,11 +79,7 @@ impl Drop for ConnectionHandler { } impl ConnectionHandler { - pub(crate) fn new( - shared: &SharedData, - tcp_stream: TcpStream, - remote_address: SocketAddr, - ) -> Self { + pub(crate) fn new(shared: &SharedData, remote_address: SocketAddr) -> Self { let shutdown = shared.shutdown.child_token(remote_address.to_string()); shared.metrics.network.new_active_ingress_mixnet_client(); @@ -93,11 +90,11 @@ impl ConnectionHandler { replay_protection_filter: shared.replay_protection_filter.clone(), mixnet_forwarder: shared.mixnet_forwarder.clone(), final_hop: shared.final_hop.clone(), + noise_config: shared.noise_config.clone(), metrics: shared.metrics.clone(), shutdown, }, remote_address, - mixnet_connection: Framed::new(tcp_stream, NymCodec), pending_packets: PendingReplayCheckPackets::new(), } } @@ -365,7 +362,29 @@ impl ConnectionHandler { remote = %self.remote_address ) )] - pub(crate) async fn handle_stream(&mut self) { + pub(crate) async fn handle_connection(&mut self, socket: TcpStream) { + let noise_stream = match upgrade_noise_responder(socket, &self.shared.noise_config).await { + Ok(noise_stream) => noise_stream, + Err(err) => { + error!( + "Failed to perform Noise handshake with {:?} - {err}", + self.remote_address + ); + return; + } + }; + debug!( + "Noise responder handshake completed for {:?}", + self.remote_address + ); + self.handle_stream(Framed::new(noise_stream, NymCodec)) + .await + } + + pub(crate) async fn handle_stream( + &mut self, + mut mixnet_connection: Framed, + ) { loop { tokio::select! { biased; @@ -373,7 +392,7 @@ impl ConnectionHandler { trace!("connection handler: received shutdown"); break } - maybe_framed_nym_packet = self.mixnet_connection.next() => { + maybe_framed_nym_packet = mixnet_connection.next() => { match maybe_framed_nym_packet { Some(Ok(packet)) => self.handle_received_nym_packet(packet).await, Some(Err(err)) => { diff --git a/nym-node/src/node/mixnet/shared/mod.rs b/nym-node/src/node/mixnet/shared/mod.rs index 13f6922eaff..c10e2d4c639 100644 --- a/nym-node/src/node/mixnet/shared/mod.rs +++ b/nym-node/src/node/mixnet/shared/mod.rs @@ -10,6 +10,7 @@ use nym_gateway::node::GatewayStorageError; use nym_mixnet_client::forwarder::{MixForwardingSender, PacketToForward}; use nym_node_metrics::mixnet::PacketKind; use nym_node_metrics::NymNodeMetrics; +use nym_noise::config::NoiseConfig; use nym_sphinx_forwarding::packet::MixPacket; use nym_sphinx_framing::processing::{ MixPacketVersion, MixProcessingResult, MixProcessingResultData, PacketProcessingError, @@ -75,6 +76,9 @@ pub(crate) struct SharedData { // data specific to the final hop (gateway) processing pub(super) final_hop: SharedFinalHopData, + // for establishing a Noise connection + pub(super) noise_config: NoiseConfig, + pub(super) metrics: NymNodeMetrics, pub(super) shutdown: ShutdownToken, } @@ -93,6 +97,7 @@ impl SharedData { replay_protection_filter: ReplayProtectionBloomfilter, mixnet_forwarder: MixForwardingSender, final_hop: SharedFinalHopData, + noise_config: NoiseConfig, metrics: NymNodeMetrics, shutdown: ShutdownToken, ) -> Self { @@ -102,6 +107,7 @@ impl SharedData { replay_protection_filter, mixnet_forwarder, final_hop, + noise_config, metrics, shutdown, } @@ -164,8 +170,9 @@ impl SharedData { match accepted { Ok((socket, remote_addr)) => { debug!("accepted incoming mixnet connection from: {remote_addr}"); - let mut handler = ConnectionHandler::new(self, socket, remote_addr); - let join_handle = tokio::spawn(async move { handler.handle_stream().await }); + let mut handler = ConnectionHandler::new(self, remote_addr); + let join_handle = + tokio::spawn(async move { handler.handle_connection(socket).await }); self.log_connected_clients(); Some(join_handle) } From 8f950eece4105a6e2073982fa5e15e67f6b34454 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Tue, 20 May 2025 16:07:21 +0200 Subject: [PATCH 06/21] noise handshake initiator side --- common/client-libs/mixnet-client/Cargo.toml | 7 +++- .../client-libs/mixnet-client/src/client.rs | 39 +++++++++++++++++-- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/common/client-libs/mixnet-client/Cargo.toml b/common/client-libs/mixnet-client/Cargo.toml index b240aab5131..2a8a39383b6 100644 --- a/common/client-libs/mixnet-client/Cargo.toml +++ b/common/client-libs/mixnet-client/Cargo.toml @@ -16,9 +16,14 @@ tokio-util = { workspace = true, features = ["codec"], optional = true } tokio-stream = { workspace = true } # internal +nym-noise = { path = "../../nymnoise" } nym-sphinx = { path = "../../nymsphinx" } nym-task = { path = "../../task", optional = true } [features] default = ["client"] -client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"] \ No newline at end of file +client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"] + +[dev-dependencies] +nym-crypto = { path = "../../crypto" } +rand = { workspace = true } diff --git a/common/client-libs/mixnet-client/src/client.rs b/common/client-libs/mixnet-client/src/client.rs index 8788a0aee70..e294a2683ba 100644 --- a/common/client-libs/mixnet-client/src/client.rs +++ b/common/client-libs/mixnet-client/src/client.rs @@ -3,6 +3,8 @@ use dashmap::DashMap; use futures::StreamExt; +use nym_noise::config::NoiseConfig; +use nym_noise::upgrade_noise_initiator; use nym_sphinx::addressing::nodes::NymNodeRoutingAddress; use nym_sphinx::framing::codec::NymCodec; use nym_sphinx::framing::packet::FramedNymPacket; @@ -59,6 +61,7 @@ pub trait SendWithoutResponse { pub struct Client { active_connections: ActiveConnections, + noise_config: NoiseConfig, connections_count: Arc, config: Config, } @@ -104,6 +107,7 @@ impl ConnectionSender { struct ManagedConnection { address: SocketAddr, + noise_config: NoiseConfig, message_receiver: ReceiverStream, connection_timeout: Duration, current_reconnection: Arc, @@ -112,12 +116,14 @@ struct ManagedConnection { impl ManagedConnection { fn new( address: SocketAddr, + noise_config: NoiseConfig, message_receiver: mpsc::Receiver, connection_timeout: Duration, current_reconnection: Arc, ) -> Self { ManagedConnection { address, + noise_config, message_receiver: ReceiverStream::new(message_receiver), connection_timeout, current_reconnection, @@ -132,9 +138,21 @@ impl ManagedConnection { Ok(stream_res) => match stream_res { Ok(stream) => { debug!("Managed to establish connection to {}", self.address); - // if we managed to connect, reset the reconnection count (whatever it might have been) + + let noise_stream = + match upgrade_noise_initiator(stream, &self.noise_config).await { + Ok(noise_stream) => noise_stream, + Err(err) => { + error!("Failed to perform Noise handshake with {address} - {err}"); + // we failed to finish the noise handshake - increase reconnection attempt + self.current_reconnection.fetch_add(1, Ordering::SeqCst); + return; + } + }; + // if we managed to connect AND do the noise handshake, reset the reconnection count (whatever it might have been) self.current_reconnection.store(0, Ordering::Release); - Framed::new(stream, NymCodec) + debug!("Noise initiator handshake completed for {:?}", address); + Framed::new(noise_stream, NymCodec) } Err(err) => { debug!("failed to establish connection to {address} (err: {err})",); @@ -167,9 +185,14 @@ impl ManagedConnection { } impl Client { - pub fn new(config: Config, connections_count: Arc) -> Client { + pub fn new( + config: Config, + noise_config: NoiseConfig, + connections_count: Arc, + ) -> Client { Client { active_connections: Default::default(), + noise_config, connections_count, config, } @@ -224,6 +247,7 @@ impl Client { let initial_connection_timeout = self.config.initial_connection_timeout; let connections_count = self.connections_count.clone(); + let noise_config = self.noise_config.clone(); tokio::spawn(async move { // before executing the manager, wait for what was specified, if anything if let Some(backoff) = backoff { @@ -234,6 +258,7 @@ impl Client { connections_count.fetch_add(1, Ordering::SeqCst); ManagedConnection::new( address.into(), + noise_config, receiver, initial_connection_timeout, current_reconnection_attempt, @@ -302,8 +327,12 @@ impl SendWithoutResponse for Client { #[cfg(test)] mod tests { use super::*; + use nym_crypto::asymmetric::x25519; + use nym_noise::config::NoiseNetworkView; + use rand::rngs::OsRng; fn dummy_client() -> Client { + let mut rng = OsRng; //for test only, so we don't care if rng source isn't crypto grade Client::new( Config { initial_reconnection_backoff: Duration::from_millis(10_000), @@ -311,6 +340,10 @@ mod tests { initial_connection_timeout: Duration::from_millis(1_500), maximum_connection_buffer_size: 128, }, + NoiseConfig::new( + Arc::new(x25519::KeyPair::new(&mut rng)), + NoiseNetworkView::new_empty(), + ), Default::default(), ) } From 690a18953a9e2927f7fab589710d4c720a30e88d Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Tue, 20 May 2025 16:07:21 +0200 Subject: [PATCH 07/21] enable noise by announcing keys --- nym-node/src/config/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index cc7fd218866..a12a7007537 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -742,8 +742,7 @@ impl Default for MixnetDebug { packet_forwarding_maximum_backoff: Self::DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF, initial_connection_timeout: Self::DEFAULT_INITIAL_CONNECTION_TIMEOUT, maximum_connection_buffer_size: Self::DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE, - // to be changed by @SW once the implementation is there - unsafe_disable_noise: true, + unsafe_disable_noise: false, } } } From bfa52f76349ed483d73b91992de4532088491fab Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Tue, 20 May 2025 16:07:21 +0200 Subject: [PATCH 08/21] fix wasm client by conditionnally import mixnet client in client-core --- common/client-core/Cargo.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/client-core/Cargo.toml b/common/client-core/Cargo.toml index b9efc9f46f8..5d4f0bbfb7f 100644 --- a/common/client-core/Cargo.toml +++ b/common/client-core/Cargo.toml @@ -44,7 +44,6 @@ nym-sphinx = { path = "../nymsphinx" } nym-statistics-common = { path = "../statistics" } nym-pemstore = { path = "../pemstore" } nym-topology = { path = "../topology", features = ["persistence"] } -nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false } nym-validator-client = { path = "../client-libs/validator-client", default-features = false } nym-task = { path = "../task" } nym-credentials-interface = { path = "../credentials-interface" } @@ -57,6 +56,9 @@ nym-client-core-surb-storage = { path = "./surb-storage" } nym-client-core-gateways-storage = { path = "./gateways-storage" } nym-ecash-time = { path = "../ecash-time" } +[target."cfg(not(target_arch = \"wasm32\"))".dependencies] +nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false } + ### For serving prometheus metrics [target."cfg(not(target_arch = \"wasm32\"))".dependencies.hyper] workspace = true From cb1731494fa22eff02fb9f4593abe45dfba20b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 20 May 2025 16:08:04 +0200 Subject: [PATCH 09/21] additional Polish; missing features, extra test, etc --- Cargo.lock | 5 +-- common/nymnoise/Cargo.toml | 6 ++- common/nymnoise/keys/Cargo.toml | 5 ++- common/nymnoise/src/config.rs | 5 ++- common/nymnoise/src/connection.rs | 4 +- common/nymnoise/src/error.rs | 8 ++-- common/nymnoise/src/lib.rs | 11 ++--- common/nymnoise/src/stream.rs | 4 +- nym-node/nym-node-requests/src/api/mod.rs | 51 +++++++++++++++++++++-- nym-wallet/Cargo.lock | 12 ++++++ 10 files changed, 85 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f0775282248..32f4992d066 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6422,16 +6422,15 @@ dependencies = [ "arc-swap", "bytes", "futures", - "log", "nym-crypto", "nym-noise-keys", "pin-project", - "serde", - "sha2 0.10.9", + "sha2 0.10.8", "snow", "thiserror 2.0.12", "tokio", "tokio-util", + "tracing", ] [[package]] diff --git a/common/nymnoise/Cargo.toml b/common/nymnoise/Cargo.toml index d1a3b001f71..07893e59b1b 100644 --- a/common/nymnoise/Cargo.toml +++ b/common/nymnoise/Cargo.toml @@ -9,9 +9,8 @@ license.workspace = true arc-swap = { workspace = true } bytes = { workspace = true } futures = { workspace = true } -log = { workspace = true } +tracing = { workspace = true } pin-project = { workspace = true } -serde = { workspace = true, features = ["derive"] } sha2 = { workspace = true } snow = { workspace = true } thiserror = { workspace = true } @@ -21,3 +20,6 @@ tokio-util = { workspace = true, features = ["codec"] } # internal nym-crypto = { path = "../crypto" } nym-noise-keys = { path = "keys" } + +[lints] +workspace = true \ No newline at end of file diff --git a/common/nymnoise/keys/Cargo.toml b/common/nymnoise/keys/Cargo.toml index 67444cd589c..94080a004b1 100644 --- a/common/nymnoise/keys/Cargo.toml +++ b/common/nymnoise/keys/Cargo.toml @@ -11,4 +11,7 @@ serde = { workspace = true, features = ["derive"] } utoipa = { workspace = true } # internal -nym-crypto = { path = "../../crypto" } +nym-crypto = { path = "../../crypto", features = ["asymmetric", "serde"] } + +[lints] +workspace = true \ No newline at end of file diff --git a/common/nymnoise/src/config.rs b/common/nymnoise/src/config.rs index 3554d25897e..fa862a404fc 100644 --- a/common/nymnoise/src/config.rs +++ b/common/nymnoise/src/config.rs @@ -1,5 +1,5 @@ // Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only +// SPDX-License-Identifier: Apache-2.0 use std::{ collections::HashMap, @@ -26,6 +26,8 @@ impl NoisePattern { } } + // SAFETY: if unwraps failed, it means hardcoded patterns were wrong + #[allow(clippy::unwrap_used)] pub(crate) fn psk_position(&self) -> u8 { //automatic parsing, works for correct pattern, more convenient match self.as_str().find("psk") { @@ -33,7 +35,6 @@ impl NoisePattern { let psk_index = n + 3; let psk_char = self.as_str().chars().nth(psk_index).unwrap(); psk_char.to_string().parse().unwrap() - //if this fails, it means hardcoded pattern are wrong } None => 0, } diff --git a/common/nymnoise/src/connection.rs b/common/nymnoise/src/connection.rs index 1c125df7a09..6f2d64fef44 100644 --- a/common/nymnoise/src/connection.rs +++ b/common/nymnoise/src/connection.rs @@ -1,5 +1,5 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 use std::io; diff --git a/common/nymnoise/src/error.rs b/common/nymnoise/src/error.rs index 12d0ce21858..5f29d966872 100644 --- a/common/nymnoise/src/error.rs +++ b/common/nymnoise/src/error.rs @@ -1,5 +1,5 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 use snow::Error; use std::io; @@ -10,10 +10,10 @@ pub enum NoiseError { #[error("encountered a Noise decryption error")] DecryptionError, - #[error("encountered a Noise Protocol error - {0}")] + #[error("encountered a Noise Protocol error: {0}")] ProtocolError(Error), - #[error("encountered an IO error - {0}")] + #[error("encountered an IO error: {0}")] IoError(#[from] io::Error), #[error("Incorrect state")] diff --git a/common/nymnoise/src/lib.rs b/common/nymnoise/src/lib.rs index 857cec53fee..a3e6f9ab3b1 100644 --- a/common/nymnoise/src/lib.rs +++ b/common/nymnoise/src/lib.rs @@ -1,16 +1,16 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 use crate::config::{NoiseConfig, NoisePattern}; use crate::connection::Connection; use crate::error::NoiseError; use crate::stream::NoiseStream; -use log::*; use nym_crypto::asymmetric::x25519; use nym_noise_keys::NoiseVersion; use sha2::{Digest, Sha256}; use snow::{error::Prerequisite, Builder, Error}; use tokio::net::TcpStream; +use tracing::*; pub mod config; pub mod connection; @@ -137,10 +137,7 @@ pub async fn upgrade_noise_responder( // Port is random and we just need the support info match config.get_noise_support(initiator_addr.ip()) { None => { - warn!( - "{:?} can't speak Noise yet, falling back to TCP", - initiator_addr - ); + warn!("{initiator_addr} can't speak Noise yet, falling back to TCP",); Ok(Connection::Tcp(conn)) } //responder's info on version is shaky, so initiator has to adapt. This behavior can change in the future diff --git a/common/nymnoise/src/stream.rs b/common/nymnoise/src/stream.rs index ef9e470cb8d..a37c371b1a2 100644 --- a/common/nymnoise/src/stream.rs +++ b/common/nymnoise/src/stream.rs @@ -1,5 +1,5 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 use crate::error::NoiseError; use bytes::BytesMut; diff --git a/nym-node/nym-node-requests/src/api/mod.rs b/nym-node/nym-node-requests/src/api/mod.rs index 49132ea6e27..5e630feb70d 100644 --- a/nym-node/nym-node-requests/src/api/mod.rs +++ b/nym-node/nym-node-requests/src/api/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::api::v1::node::models::{LegacyHostInformation, LegacyHostInformationV2}; @@ -8,7 +8,6 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::ops::Deref; -use utoipa::ToSchema; #[cfg(feature = "client")] pub mod client; @@ -20,7 +19,7 @@ pub use client::Client; // create the type alias manually if openapi is not enabled pub type SignedHostInformation = SignedData; -#[derive(ToSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct SignedDataHostInfo { // #[serde(flatten)] pub data: crate::api::v1::node::models::HostInformation, @@ -40,6 +39,7 @@ impl SignedData { T: Serialize, { let plaintext = serde_json::to_string(&data)?; + let signature = key.sign(plaintext).to_base58_string(); Ok(SignedData { data, signature }) } @@ -66,6 +66,8 @@ impl SignedHostInformation { return true; } + // TODO: @JS: to remove downgrade support in future release(s) + // attempt to verify legacy signatures let legacy_v2 = SignedData { data: LegacyHostInformationV2::from(self.data.clone()), @@ -107,6 +109,7 @@ impl Display for ErrorResponse { mod tests { use super::*; + use crate::api::v1::node::models::HostInformation; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_noise_keys::{NoiseVersion, VersionedNoiseKey}; use rand_chacha::rand_core::SeedableRng; @@ -224,6 +227,48 @@ mod tests { assert!(!current_struct_no_noise.verify(ed22519.public_key())); assert!(current_struct_no_noise.verify_host_information()); + assert!(!current_struct_noise.verify(ed22519.public_key())); + assert!(current_struct_noise.verify_host_information()) + } + + #[test] + fn dummy_current_signed_host_verification() { + let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); + let ed22519 = ed25519::KeyPair::new(&mut rng); + let x25519_sphinx = x25519::KeyPair::new(&mut rng); + let x25519_noise = x25519::KeyPair::new(&mut rng); + + let host_info_no_noise = HostInformation { + ip_address: vec!["1.1.1.1".parse().unwrap()], + hostname: Some("foomp.com".to_string()), + keys: crate::api::v1::node::models::HostKeys { + ed25519_identity: *ed22519.public_key(), + x25519_sphinx: *x25519_sphinx.public_key(), + x25519_noise: None, + }, + }; + + let host_info_noise = crate::api::v1::node::models::HostInformation { + ip_address: vec!["1.1.1.1".parse().unwrap()], + hostname: Some("foomp.com".to_string()), + keys: crate::api::v1::node::models::HostKeys { + ed25519_identity: *ed22519.public_key(), + x25519_sphinx: *x25519_sphinx.public_key(), + x25519_noise: Some(VersionedNoiseKey { + version: NoiseVersion::V1, + x25519_pubkey: *x25519_noise.public_key(), + }), + }, + }; + + // signature on legacy data + let current_struct_no_noise = + SignedData::new(host_info_no_noise, ed22519.private_key()).unwrap(); + let current_struct_noise = SignedData::new(host_info_noise, ed22519.private_key()).unwrap(); + + assert!(current_struct_no_noise.verify(ed22519.public_key())); + assert!(current_struct_no_noise.verify_host_information()); + // if noise key is present, the signature is actually valid assert!(current_struct_noise.verify(ed22519.public_key())); assert!(current_struct_noise.verify_host_information()) diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 211d475f059..2ebbe10c58f 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -4017,6 +4017,7 @@ dependencies = [ "nym-mixnet-contract-common", "nym-network-defaults", "nym-node-requests", + "nym-noise-keys", "nym-serde-helpers", "nym-ticketbooks-merkle", "schemars", @@ -4277,6 +4278,7 @@ dependencies = [ "nym-crypto", "nym-exit-policy", "nym-http-api-client", + "nym-noise-keys", "nym-wireguard-types", "schemars", "serde", @@ -4287,6 +4289,16 @@ dependencies = [ "utoipa", ] +[[package]] +name = "nym-noise-keys" +version = "0.1.0" +dependencies = [ + "nym-crypto", + "schemars", + "serde", + "utoipa", +] + [[package]] name = "nym-pemstore" version = "0.3.0" From c6675bb2070374a40eb5faaee2a7b9c6d4389809 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Tue, 20 May 2025 16:08:04 +0200 Subject: [PATCH 10/21] some comments and minor improvements for future versions --- common/nymnoise/keys/src/lib.rs | 4 +- common/nymnoise/src/connection.rs | 1 + common/nymnoise/src/lib.rs | 73 +++++++++++++------------------ nym-node/src/node/mod.rs | 2 +- 4 files changed, 34 insertions(+), 46 deletions(-) diff --git a/common/nymnoise/keys/src/lib.rs b/common/nymnoise/keys/src/lib.rs index 00a0fbfd39c..93dca05fbfc 100644 --- a/common/nymnoise/keys/src/lib.rs +++ b/common/nymnoise/keys/src/lib.rs @@ -5,11 +5,11 @@ use nym_crypto::asymmetric::x25519; use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey; use serde::{Deserialize, Serialize}; -#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(from = "u8", into = "u8")] pub enum NoiseVersion { V1 = 1, - Unknown, + Unknown, //Implies a newer version we don't know } impl From for NoiseVersion { diff --git a/common/nymnoise/src/connection.rs b/common/nymnoise/src/connection.rs index 6f2d64fef44..79f3533690f 100644 --- a/common/nymnoise/src/connection.rs +++ b/common/nymnoise/src/connection.rs @@ -11,6 +11,7 @@ use tokio::{ use crate::stream::NoiseStream; +//SW once plain TCP support is dropped, this whole enum can be dropped, and we can only propagate NoiseStream #[pin_project(project = ConnectionProj)] pub enum Connection { Tcp(#[pin] TcpStream), diff --git a/common/nymnoise/src/lib.rs b/common/nymnoise/src/lib.rs index a3e6f9ab3b1..b55958e27e5 100644 --- a/common/nymnoise/src/lib.rs +++ b/common/nymnoise/src/lib.rs @@ -1,7 +1,7 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::config::{NoiseConfig, NoisePattern}; +use crate::config::NoiseConfig; use crate::connection::Connection; use crate::error::NoiseError; use crate::stream::NoiseStream; @@ -19,12 +19,11 @@ pub mod stream; const NOISE_PSK_PREFIX: &[u8] = b"NYMTECH_NOISE_dQw4w9WgXcQ"; -pub const NOISE_VERSION: NoiseVersion = NoiseVersion::V1; +pub const LATEST_NOISE_VERSION: NoiseVersion = NoiseVersion::V1; async fn upgrade_noise_initiator_v1( conn: TcpStream, - pattern: NoisePattern, - local_private_key: &x25519::PrivateKey, + config: &NoiseConfig, remote_pub_key: &x25519::PublicKey, ) -> Result { trace!("Perform Noise Handshake, initiator side"); @@ -36,10 +35,10 @@ async fn upgrade_noise_initiator_v1( .concat(); let secret_hash = Sha256::digest(secret); - let handshake = Builder::new(pattern.as_str().parse()?) - .local_private_key(&local_private_key.to_bytes()) + let handshake = Builder::new(config.pattern.as_str().parse()?) + .local_private_key(&config.local_key.private_key().to_bytes()) .remote_public_key(&remote_pub_key.to_bytes()) - .psk(pattern.psk_position(), &secret_hash) + .psk(config.pattern.psk_position(), &secret_hash) .build_initiator()?; let noise_stream = NoiseStream::new(conn, handshake); @@ -64,28 +63,18 @@ pub async fn upgrade_noise_initiator( match config.get_noise_key(&responder_addr) { Some(key) => match key.version { - NoiseVersion::V1 => { - upgrade_noise_initiator_v1( - conn, - config.pattern, - config.local_key.private_key(), - &key.x25519_pubkey, - ) - .await - } + NoiseVersion::V1 => upgrade_noise_initiator_v1(conn, config, &key.x25519_pubkey).await, + // We're talking to a more recent node, but we can't adapt. Let's try to do our best and if it fails, it fails. + // If that node sees we're older, it will try to adapt too. NoiseVersion::Unknown => { - error!( - "{:?} is announcing an unknown version of Noise", - responder_addr - ); - Err(NoiseError::UnknownVersion) + warn!("{responder_addr} is announcing an unknown version of Noise, we will still attempt our latest known version"); + upgrade_noise_initiator_v1(conn, config, &key.x25519_pubkey) + .await + .or(Err(NoiseError::UnknownVersion)) } }, None => { - warn!( - "{:?} can't speak Noise yet, falling back to TCP", - responder_addr - ); + warn!("{responder_addr} can't speak Noise yet, falling back to TCP"); Ok(Connection::Tcp(conn)) } } @@ -93,22 +82,20 @@ pub async fn upgrade_noise_initiator( async fn upgrade_noise_responder_v1( conn: TcpStream, - pattern: NoisePattern, - local_public_key: &x25519::PublicKey, - local_private_key: &x25519::PrivateKey, + config: &NoiseConfig, ) -> Result { trace!("Perform Noise Handshake, responder side"); let secret = [ NOISE_PSK_PREFIX.to_vec(), - local_public_key.to_bytes().to_vec(), + config.local_key.public_key().to_bytes().to_vec(), ] .concat(); let secret_hash = Sha256::digest(secret); - let handshake = Builder::new(pattern.as_str().parse()?) - .local_private_key(&local_private_key.to_bytes()) - .psk(pattern.psk_position(), &secret_hash) + let handshake = Builder::new(config.pattern.as_str().parse()?) + .local_private_key(&config.local_key.private_key().to_bytes()) + .psk(config.pattern.psk_position(), &secret_hash) .build_responder()?; let noise_stream = NoiseStream::new(conn, handshake); @@ -140,16 +127,16 @@ pub async fn upgrade_noise_responder( warn!("{initiator_addr} can't speak Noise yet, falling back to TCP",); Ok(Connection::Tcp(conn)) } - //responder's info on version is shaky, so initiator has to adapt. This behavior can change in the future - Some(_) => { - //Existing node supporting Noise - upgrade_noise_responder_v1( - conn, - config.pattern, - config.local_key.public_key(), - config.local_key.private_key(), - ) - .await - } + // responder's info on version is shaky, so ideally, initiator has to adapt. + // if we are newer, it won't ba able to, so let's try to meet him on his ground. + Some(LATEST_NOISE_VERSION) | Some(NoiseVersion::Unknown) => { + // Node is announcing the same version as us, great or + // Node is announcing a newer version than us, it should adapt to us though + upgrade_noise_responder_v1(conn, config).await + } //SW sample of code to allow backwards compatibility when we introduce new versions + // Some(IntermediateNoiseVersion) => { + // Node is announcing an older version, let's try to adapt + // upgrade_noise_responder_Vwhatever + // } } } diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index 7cfd26ea439..d880c9125b4 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -703,7 +703,7 @@ impl NymNode { &self.config, self.x25519_sphinx_keys.public_key(), &VersionedNoiseKey { - version: nym_noise::NOISE_VERSION, + version: nym_noise::LATEST_NOISE_VERSION, x25519_pubkey: *self.x25519_noise_keys.public_key(), }, &self.ed25519_identity_keys, From cc28575176184c46534140e5bac8b02602fd28cd Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Tue, 20 May 2025 16:08:04 +0200 Subject: [PATCH 11/21] appease the clippy god --- Cargo.lock | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 32f4992d066..c4ce420cc95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6000,8 +6000,11 @@ version = "0.1.0" dependencies = [ "dashmap", "futures", + "nym-crypto", + "nym-noise", "nym-sphinx", "nym-task", + "rand 0.8.5", "tokio", "tokio-stream", "tokio-util", From 7e2e122725b397aa4cbf777cb3a6714ed8f59398 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Tue, 20 May 2025 16:08:05 +0200 Subject: [PATCH 12/21] appease the clippy god --- nym-node/src/node/mixnet/shared/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/nym-node/src/node/mixnet/shared/mod.rs b/nym-node/src/node/mixnet/shared/mod.rs index c10e2d4c639..1d832c90295 100644 --- a/nym-node/src/node/mixnet/shared/mod.rs +++ b/nym-node/src/node/mixnet/shared/mod.rs @@ -91,6 +91,7 @@ fn convert_to_metrics_version(processed: MixPacketVersion) -> PacketKind { } impl SharedData { + #[allow(clippy::too_many_arguments)] pub(crate) fn new( processing_config: ProcessingConfig, x25519_keys: Arc, From 162b74293ed5b688cc4a2cdebeea71a79e785466 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Tue, 20 May 2025 16:08:05 +0200 Subject: [PATCH 13/21] resolve non stream-related PR comments --- Cargo.lock | 1 + common/nymnoise/Cargo.toml | 1 + common/nymnoise/src/config.rs | 20 ++++++-------------- common/nymnoise/src/lib.rs | 24 +++++++++++------------- 4 files changed, 19 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c4ce420cc95..2bbedc4d15d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6434,6 +6434,7 @@ dependencies = [ "tokio", "tokio-util", "tracing", + "zeroize", ] [[package]] diff --git a/common/nymnoise/Cargo.toml b/common/nymnoise/Cargo.toml index 07893e59b1b..0b437eb5b28 100644 --- a/common/nymnoise/Cargo.toml +++ b/common/nymnoise/Cargo.toml @@ -16,6 +16,7 @@ snow = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["net", "io-util", "time"] } tokio-util = { workspace = true, features = ["codec"] } +zeroize = { workspace = true } # internal nym-crypto = { path = "../crypto" } diff --git a/common/nymnoise/src/config.rs b/common/nymnoise/src/config.rs index fa862a404fc..18e4da3bbdc 100644 --- a/common/nymnoise/src/config.rs +++ b/common/nymnoise/src/config.rs @@ -115,19 +115,11 @@ impl NoiseConfig { // Only for phased update //SW This can lead to some troubles if two nodes shares the same IP and one support Noise but not the other. This in only for the progressive update though and there is no workaround pub(crate) fn get_noise_support(&self, ip_addr: IpAddr) -> Option { - self.network - .support - .inner - .load() - .get(&ip_addr) - .copied() - .or_else(|| { - self.network - .support - .inner - .load() - .get(&ip_addr.to_canonical()) // SW default bind address being [::]:1789, it can happen that a responder sees the ipv6-mapped address of the initiator, this check for that - .copied() - }) + let plain_ip_support = self.network.support.inner.load().get(&ip_addr).copied(); + + // SW default bind address being [::]:1789, it can happen that a responder sees the ipv6-mapped address of the initiator, this check for that + let canonical_ip = &ip_addr.to_canonical(); + let canonical_ip_support = self.network.support.inner.load().get(canonical_ip).copied(); + plain_ip_support.or(canonical_ip_support) } } diff --git a/common/nymnoise/src/lib.rs b/common/nymnoise/src/lib.rs index b55958e27e5..0728ac3afa0 100644 --- a/common/nymnoise/src/lib.rs +++ b/common/nymnoise/src/lib.rs @@ -11,6 +11,7 @@ use sha2::{Digest, Sha256}; use snow::{error::Prerequisite, Builder, Error}; use tokio::net::TcpStream; use tracing::*; +use zeroize::Zeroizing; pub mod config; pub mod connection; @@ -21,6 +22,13 @@ const NOISE_PSK_PREFIX: &[u8] = b"NYMTECH_NOISE_dQw4w9WgXcQ"; pub const LATEST_NOISE_VERSION: NoiseVersion = NoiseVersion::V1; +fn generate_psk_v1(responder_pub_key: &x25519::PublicKey) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(NOISE_PSK_PREFIX); + hasher.update(responder_pub_key.to_bytes()); + hasher.finalize().into() +} + async fn upgrade_noise_initiator_v1( conn: TcpStream, config: &NoiseConfig, @@ -28,15 +36,10 @@ async fn upgrade_noise_initiator_v1( ) -> Result { trace!("Perform Noise Handshake, initiator side"); - let secret = [ - NOISE_PSK_PREFIX.to_vec(), - remote_pub_key.to_bytes().to_vec(), - ] - .concat(); - let secret_hash = Sha256::digest(secret); + let secret_hash = generate_psk_v1(remote_pub_key); let handshake = Builder::new(config.pattern.as_str().parse()?) - .local_private_key(&config.local_key.private_key().to_bytes()) + .local_private_key(Zeroizing::new(config.local_key.private_key().to_bytes()).as_ref()) .remote_public_key(&remote_pub_key.to_bytes()) .psk(config.pattern.psk_position(), &secret_hash) .build_initiator()?; @@ -86,12 +89,7 @@ async fn upgrade_noise_responder_v1( ) -> Result { trace!("Perform Noise Handshake, responder side"); - let secret = [ - NOISE_PSK_PREFIX.to_vec(), - config.local_key.public_key().to_bytes().to_vec(), - ] - .concat(); - let secret_hash = Sha256::digest(secret); + let secret_hash = generate_psk_v1(config.local_key.public_key()); let handshake = Builder::new(config.pattern.as_str().parse()?) .local_private_key(&config.local_key.private_key().to_bytes()) From cc4dcffc42a263c44d7fb8ad7082d8b143713937 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Tue, 20 May 2025 16:08:05 +0200 Subject: [PATCH 14/21] fix asyncread and asyncwrite op following PR comment --- common/nymnoise/src/stream.rs | 49 +++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/common/nymnoise/src/stream.rs b/common/nymnoise/src/stream.rs index a37c371b1a2..49766dc02c7 100644 --- a/common/nymnoise/src/stream.rs +++ b/common/nymnoise/src/stream.rs @@ -7,7 +7,6 @@ use futures::{Sink, SinkExt, Stream, StreamExt}; use pin_project::pin_project; use snow::{HandshakeState, TransportState}; use std::cmp::min; -use std::collections::VecDeque; use std::io; use std::pin::Pin; use std::task::Poll; @@ -27,7 +26,7 @@ pub struct NoiseStream { inner_stream: Framed, handshake: Option, noise: Option, - dec_buffer: VecDeque, + dec_buffer: BytesMut, } impl NoiseStream { @@ -38,7 +37,7 @@ impl NoiseStream { .new_framed(inner_stream), handshake: Some(handshake), noise: None, - dec_buffer: VecDeque::with_capacity(MAXMSGLEN), + dec_buffer: BytesMut::with_capacity(MAXMSGLEN), } } @@ -100,10 +99,11 @@ impl AsyncRead for NoiseStream { ) -> Poll> { let projected_self = self.project(); - match projected_self.inner_stream.poll_next(cx) { + let pending = match projected_self.inner_stream.poll_next(cx) { Poll::Pending => { - //no new data, waking is already scheduled. + //no new data, a return value of Poll::Pending means the waking is already scheduled //Nothing new to decrypt, only check if we can return something from dec_storage, happens after + true } Poll::Ready(Some(Ok(noise_msg))) => { @@ -119,28 +119,31 @@ impl AsyncRead for NoiseStream { None => return Poll::Ready(Err(io::ErrorKind::Other.into())), }; projected_self.dec_buffer.extend(&dec_msg[..len]); + false } Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(err)), - //Stream is done, return Ok with nothing in buf - Poll::Ready(None) => return Poll::Ready(Ok(())), - } + Poll::Ready(None) => { + //Stream is done, we might still have data in the buffer though, happens afterwards + false + } + }; - //check and return what we can + // Checking if there is something to return from the buffer let read_len = min(buf.remaining(), projected_self.dec_buffer.len()); if read_len > 0 { - buf.put_slice( - &projected_self - .dec_buffer - .drain(..read_len) - .collect::>(), - ); + buf.put_slice(&projected_self.dec_buffer.split_to(read_len)); return Poll::Ready(Ok(())); } - //If we end up here, it must mean the previous poll_next was pending as well, otherwise something was returned. Hence waking is already scheduled - Poll::Pending + // buf.remaining == 0 or nothing in the buffer, we must return the value we had from the inner_stream + if pending { + //If we end up here, it means the previous poll_next was pending as well, hence waking is already scheduled + Poll::Pending + } else { + Poll::Ready(Ok(())) + } } } @@ -167,8 +170,16 @@ impl AsyncWrite for NoiseStream { return Poll::Ready(Err(io::ErrorKind::InvalidInput.into())); }; noise_buf.truncate(len); - match projected_self.inner_stream.start_send(noise_buf.into()) { - Ok(()) => Poll::Ready(Ok(buf.len())), + match projected_self + .inner_stream + .as_mut() + .start_send(noise_buf.into()) + { + Ok(()) => match projected_self.inner_stream.poll_flush(cx) { + Poll::Pending => Poll::Pending, // A return value of Poll::Pending means the waking is already scheduled + Poll::Ready(Err(e)) => Poll::Ready(Err(e)), + Poll::Ready(Ok(())) => Poll::Ready(Ok(buf.len())), + }, Err(e) => Poll::Ready(Err(e)), } } From cd61e7c903142f2b1ef7bbb697bef00fba34cec7 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Tue, 20 May 2025 16:08:05 +0200 Subject: [PATCH 15/21] add active_only option for semi-skimmed node build_response --- nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs b/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs index 8b74396411a..572ba25a193 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs @@ -28,6 +28,7 @@ fn build_nym_nodes_response<'a, NI>( rewarded_set: &CachedEpochRewardedSet, nym_nodes_subset: NI, annotations: &HashMap, + active_only: bool, ) -> Vec where NI: Iterator + 'a, @@ -38,6 +39,11 @@ where let role: NodeRole = rewarded_set.role(node_id).into(); + // if the role is inactive, see if our filter allows it + if active_only && role.is_inactive() { + continue; + } + // honestly, not sure under what exact circumstances this value could be missing, // but in that case just use 0 performance let annotation = annotations.get(&node_id).copied().unwrap_or_default(); @@ -109,7 +115,7 @@ pub(super) async fn nodes_expanded( let legacy_mixnodes = state.legacy_mixnode_annotations().await?; let legacy_gateways = state.legacy_gateways_annotations().await?; - let mut nodes = build_nym_nodes_response(&rewarded_set, all_nym_nodes, &annotations); + let mut nodes = build_nym_nodes_response(&rewarded_set, all_nym_nodes, &annotations, false); // add legacy gateways to the response add_legacy(&mut nodes, &rewarded_set, &describe_cache, &legacy_gateways); From 32fc3c1e7014e97f1ed9461f9b6664a21286c597 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Tue, 20 May 2025 16:08:05 +0200 Subject: [PATCH 16/21] improve noisestream creation and test noisepatterns --- Cargo.lock | 2 +- common/crypto/src/asymmetric/x25519/mod.rs | 4 +++ common/nymnoise/Cargo.toml | 4 +-- common/nymnoise/src/config.rs | 41 ++++++++++++++++++++-- common/nymnoise/src/lib.rs | 20 ++--------- common/nymnoise/src/stream.rs | 40 +++++++++++++++++---- 6 files changed, 83 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2bbedc4d15d..14d243eca79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6430,11 +6430,11 @@ dependencies = [ "pin-project", "sha2 0.10.8", "snow", + "strum 0.26.3", "thiserror 2.0.12", "tokio", "tokio-util", "tracing", - "zeroize", ] [[package]] diff --git a/common/crypto/src/asymmetric/x25519/mod.rs b/common/crypto/src/asymmetric/x25519/mod.rs index 4b580f289b0..c17555755d4 100644 --- a/common/crypto/src/asymmetric/x25519/mod.rs +++ b/common/crypto/src/asymmetric/x25519/mod.rs @@ -256,6 +256,10 @@ impl PrivateKey { self.0.to_bytes() } + pub fn as_bytes(&self) -> &[u8; PRIVATE_KEY_SIZE] { + self.0.as_bytes() + } + pub fn from_bytes(b: &[u8]) -> Result { if b.len() != PRIVATE_KEY_SIZE { return Err(KeyRecoveryError::InvalidSizePrivateKey { diff --git a/common/nymnoise/Cargo.toml b/common/nymnoise/Cargo.toml index 0b437eb5b28..2c06cf72cb8 100644 --- a/common/nymnoise/Cargo.toml +++ b/common/nymnoise/Cargo.toml @@ -13,14 +13,14 @@ tracing = { workspace = true } pin-project = { workspace = true } sha2 = { workspace = true } snow = { workspace = true } +strum = { workspace = true, features = ["derive"] } thiserror = { workspace = true } tokio = { workspace = true, features = ["net", "io-util", "time"] } tokio-util = { workspace = true, features = ["codec"] } -zeroize = { workspace = true } # internal nym-crypto = { path = "../crypto" } nym-noise-keys = { path = "keys" } [lints] -workspace = true \ No newline at end of file +workspace = true diff --git a/common/nymnoise/src/config.rs b/common/nymnoise/src/config.rs index 18e4da3bbdc..3be3280b8f1 100644 --- a/common/nymnoise/src/config.rs +++ b/common/nymnoise/src/config.rs @@ -10,8 +10,11 @@ use std::{ use arc_swap::ArcSwap; use nym_crypto::asymmetric::x25519; use nym_noise_keys::{NoiseVersion, VersionedNoiseKey}; +use snow::params::NoiseParams; -#[derive(Default, Debug, Clone, Copy)] +use strum::EnumIter; + +#[derive(Default, Debug, Clone, Copy, EnumIter)] pub enum NoisePattern { #[default] XKpsk3, @@ -26,7 +29,7 @@ impl NoisePattern { } } - // SAFETY: if unwraps failed, it means hardcoded patterns were wrong + // SAFETY: we have tests to ensure that hardcoded pattern are correct #[allow(clippy::unwrap_used)] pub(crate) fn psk_position(&self) -> u8 { //automatic parsing, works for correct pattern, more convenient @@ -39,6 +42,12 @@ impl NoisePattern { None => 0, } } + + // SAFETY : we have tests to ensure that hardcoded pattern are correct + #[allow(clippy::unwrap_used)] + pub(crate) fn as_noise_params(&self) -> NoiseParams { + self.as_str().parse().unwrap() + } } #[derive(Debug, Default)] @@ -123,3 +132,31 @@ impl NoiseConfig { plain_ip_support.or(canonical_ip_support) } } + +#[cfg(test)] +mod tests { + use snow::params::NoiseParams; + + use super::NoisePattern; + use std::str::FromStr; + use strum::IntoEnumIterator; + + // The goal of these is to make sure every NoisePatterns are correct and unwrap can be used on them + + #[test] + fn noise_patterns_are_valid() { + for pattern in NoisePattern::iter() { + assert!(NoiseParams::from_str(pattern.as_str()).is_ok()) + } + } + + #[test] + fn noise_patterns_psk_position_is_valid() { + for pattern in NoisePattern::iter() { + match pattern { + NoisePattern::XKpsk3 => assert_eq!(pattern.psk_position(), 3), + NoisePattern::IKpsk2 => assert_eq!(pattern.psk_position(), 2), + } + } + } +} diff --git a/common/nymnoise/src/lib.rs b/common/nymnoise/src/lib.rs index 0728ac3afa0..3276282dfe5 100644 --- a/common/nymnoise/src/lib.rs +++ b/common/nymnoise/src/lib.rs @@ -8,10 +8,9 @@ use crate::stream::NoiseStream; use nym_crypto::asymmetric::x25519; use nym_noise_keys::NoiseVersion; use sha2::{Digest, Sha256}; -use snow::{error::Prerequisite, Builder, Error}; +use snow::{error::Prerequisite, Error}; use tokio::net::TcpStream; use tracing::*; -use zeroize::Zeroizing; pub mod config; pub mod connection; @@ -37,14 +36,7 @@ async fn upgrade_noise_initiator_v1( trace!("Perform Noise Handshake, initiator side"); let secret_hash = generate_psk_v1(remote_pub_key); - - let handshake = Builder::new(config.pattern.as_str().parse()?) - .local_private_key(Zeroizing::new(config.local_key.private_key().to_bytes()).as_ref()) - .remote_public_key(&remote_pub_key.to_bytes()) - .psk(config.pattern.psk_position(), &secret_hash) - .build_initiator()?; - - let noise_stream = NoiseStream::new(conn, handshake); + let noise_stream = NoiseStream::new_initiator(conn, config, remote_pub_key, &secret_hash)?; Ok(Connection::Noise(noise_stream.perform_handshake().await?)) } @@ -90,13 +82,7 @@ async fn upgrade_noise_responder_v1( trace!("Perform Noise Handshake, responder side"); let secret_hash = generate_psk_v1(config.local_key.public_key()); - - let handshake = Builder::new(config.pattern.as_str().parse()?) - .local_private_key(&config.local_key.private_key().to_bytes()) - .psk(config.pattern.psk_position(), &secret_hash) - .build_responder()?; - - let noise_stream = NoiseStream::new(conn, handshake); + let noise_stream = NoiseStream::new_responder(conn, config, &secret_hash)?; Ok(Connection::Noise(noise_stream.perform_handshake().await?)) } diff --git a/common/nymnoise/src/stream.rs b/common/nymnoise/src/stream.rs index 49766dc02c7..a51f1f87fb4 100644 --- a/common/nymnoise/src/stream.rs +++ b/common/nymnoise/src/stream.rs @@ -1,11 +1,12 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::error::NoiseError; +use crate::{config::NoiseConfig, error::NoiseError}; use bytes::BytesMut; use futures::{Sink, SinkExt, Stream, StreamExt}; +use nym_crypto::asymmetric::x25519; use pin_project::pin_project; -use snow::{HandshakeState, TransportState}; +use snow::{Builder, HandshakeState, TransportState}; use std::cmp::min; use std::io; use std::pin::Pin; @@ -19,6 +20,8 @@ use tokio_util::codec::{Framed, LengthDelimitedCodec}; const MAXMSGLEN: usize = 65535; const TAGLEN: usize = 16; +pub(crate) type Psk = [u8; 32]; + /// Wrapper around a TcpStream #[pin_project] pub struct NoiseStream { @@ -30,7 +33,33 @@ pub struct NoiseStream { } impl NoiseStream { - pub(crate) fn new(inner_stream: TcpStream, handshake: HandshakeState) -> NoiseStream { + pub(crate) fn new_initiator( + inner_stream: TcpStream, + config: &NoiseConfig, + remote_pub_key: &x25519::PublicKey, + psk: &Psk, + ) -> Result { + let handshake = Builder::new(config.pattern.as_noise_params()) + .local_private_key(config.local_key.private_key().as_bytes()) + .remote_public_key(&remote_pub_key.to_bytes()) + .psk(config.pattern.psk_position(), psk) + .build_initiator()?; + Ok(NoiseStream::new_inner(inner_stream, handshake)) + } + + pub(crate) fn new_responder( + inner_stream: TcpStream, + config: &NoiseConfig, + psk: &Psk, + ) -> Result { + let handshake = Builder::new(config.pattern.as_noise_params()) + .local_private_key(config.local_key.private_key().as_bytes()) + .psk(config.pattern.psk_position(), psk) + .build_responder()?; + Ok(NoiseStream::new_inner(inner_stream, handshake)) + } + + fn new_inner(inner_stream: TcpStream, handshake: HandshakeState) -> NoiseStream { NoiseStream { inner_stream: LengthDelimitedCodec::builder() .length_field_type::() @@ -43,10 +72,9 @@ impl NoiseStream { pub(crate) async fn perform_handshake(mut self) -> Result { //Check if we are in the correct state - let Some(mut handshake) = self.handshake else { + let Some(mut handshake) = self.handshake.take() else { return Err(NoiseError::IncorrectStateError); }; - self.handshake = None; while !handshake.is_handshake_finished() { if handshake.is_my_turn() { @@ -107,7 +135,7 @@ impl AsyncRead for NoiseStream { } Poll::Ready(Some(Ok(noise_msg))) => { - //We have a new moise msg + // We have a new noise msg let mut dec_msg = vec![0u8; MAXMSGLEN]; let len = match projected_self.noise { Some(transport_state) => { From 7c104173b0c2094cc3e255b379d645429933af33 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Tue, 20 May 2025 16:08:05 +0200 Subject: [PATCH 17/21] restore start_send use --- common/nymnoise/src/stream.rs | 48 +++++++++++++++-------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/common/nymnoise/src/stream.rs b/common/nymnoise/src/stream.rs index a51f1f87fb4..500bf64ee3d 100644 --- a/common/nymnoise/src/stream.rs +++ b/common/nymnoise/src/stream.rs @@ -7,10 +7,10 @@ use futures::{Sink, SinkExt, Stream, StreamExt}; use nym_crypto::asymmetric::x25519; use pin_project::pin_project; use snow::{Builder, HandshakeState, TransportState}; -use std::cmp::min; use std::io; use std::pin::Pin; use std::task::Poll; +use std::{cmp::min, task::ready}; use tokio::{ io::{AsyncRead, AsyncWrite, ReadBuf}, net::TcpStream, @@ -183,34 +183,28 @@ impl AsyncWrite for NoiseStream { ) -> Poll> { let mut projected_self = self.project(); - match projected_self.inner_stream.as_mut().poll_ready(cx) { - Poll::Pending => Poll::Pending, + // returns on Poll::Pending and Poll:Ready(Err) + ready!(projected_self.inner_stream.as_mut().poll_ready(cx))?; - Poll::Ready(Err(err)) => Poll::Ready(Err(err)), + // Ready to send, encrypting message + let mut noise_buf = BytesMut::zeroed(MAXMSGLEN + TAGLEN); - Poll::Ready(Ok(())) => { - let mut noise_buf = BytesMut::zeroed(MAXMSGLEN + TAGLEN); - - let Ok(len) = (match projected_self.noise { - Some(transport_state) => transport_state.write_message(buf, &mut noise_buf), - None => return Poll::Ready(Err(io::ErrorKind::Other.into())), - }) else { - return Poll::Ready(Err(io::ErrorKind::InvalidInput.into())); - }; - noise_buf.truncate(len); - match projected_self - .inner_stream - .as_mut() - .start_send(noise_buf.into()) - { - Ok(()) => match projected_self.inner_stream.poll_flush(cx) { - Poll::Pending => Poll::Pending, // A return value of Poll::Pending means the waking is already scheduled - Poll::Ready(Err(e)) => Poll::Ready(Err(e)), - Poll::Ready(Ok(())) => Poll::Ready(Ok(buf.len())), - }, - Err(e) => Poll::Ready(Err(e)), - } - } + let Ok(len) = (match projected_self.noise { + Some(transport_state) => transport_state.write_message(buf, &mut noise_buf), + None => return Poll::Ready(Err(io::ErrorKind::Other.into())), + }) else { + return Poll::Ready(Err(io::ErrorKind::InvalidInput.into())); + }; + noise_buf.truncate(len); + + // Tokio uses the same `start_send ` in their SinkWriter implementation. https://docs.rs/tokio-util/latest/src/tokio_util/io/sink_writer.rs.html#104 + match projected_self + .inner_stream + .as_mut() + .start_send(noise_buf.into()) + { + Ok(()) => Poll::Ready(Ok(buf.len())), + Err(e) => Poll::Ready(Err(e)), } } From 4188c9fce4f95f6283b10ae8f225eb542aaac916 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Tue, 20 May 2025 16:08:05 +0200 Subject: [PATCH 18/21] change buffer allocation method and use connection timeout --- common/client-libs/mixnet-client/src/client.rs | 1 + common/nymnoise/src/config.rs | 9 ++++++++- common/nymnoise/src/error.rs | 3 +++ common/nymnoise/src/lib.rs | 8 ++++++-- common/nymnoise/src/stream.rs | 12 ++++++------ nym-node/src/node/mod.rs | 2 ++ 6 files changed, 26 insertions(+), 9 deletions(-) diff --git a/common/client-libs/mixnet-client/src/client.rs b/common/client-libs/mixnet-client/src/client.rs index e294a2683ba..fad3ca4c831 100644 --- a/common/client-libs/mixnet-client/src/client.rs +++ b/common/client-libs/mixnet-client/src/client.rs @@ -343,6 +343,7 @@ mod tests { NoiseConfig::new( Arc::new(x25519::KeyPair::new(&mut rng)), NoiseNetworkView::new_empty(), + Duration::from_millis(1_500), ), Default::default(), ) diff --git a/common/nymnoise/src/config.rs b/common/nymnoise/src/config.rs index 3be3280b8f1..5075c1d112f 100644 --- a/common/nymnoise/src/config.rs +++ b/common/nymnoise/src/config.rs @@ -5,6 +5,7 @@ use std::{ collections::HashMap, net::{IpAddr, SocketAddr}, sync::Arc, + time::Duration, }; use arc_swap::ArcSwap; @@ -91,16 +92,22 @@ pub struct NoiseConfig { pub(crate) local_key: Arc, pub(crate) pattern: NoisePattern, + pub(crate) timeout: Duration, pub(crate) unsafe_disabled: bool, // allows for nodes to not attempt to do a noise handshake, VERY UNSAFE, FOR DEBUG PURPOSE ONLY } impl NoiseConfig { - pub fn new(noise_key: Arc, network: NoiseNetworkView) -> Self { + pub fn new( + noise_key: Arc, + network: NoiseNetworkView, + timeout: Duration, + ) -> Self { NoiseConfig { network, local_key: noise_key, pattern: Default::default(), + timeout, unsafe_disabled: false, } } diff --git a/common/nymnoise/src/error.rs b/common/nymnoise/src/error.rs index 5f29d966872..5df1bda2f29 100644 --- a/common/nymnoise/src/error.rs +++ b/common/nymnoise/src/error.rs @@ -24,6 +24,9 @@ pub enum NoiseError { #[error("Unknown noise version")] UnknownVersion, + + #[error("Handshake timeout")] + HandshakeTimeout(#[from] tokio::time::error::Elapsed), } impl From for NoiseError { diff --git a/common/nymnoise/src/lib.rs b/common/nymnoise/src/lib.rs index 3276282dfe5..f5ce5f4803e 100644 --- a/common/nymnoise/src/lib.rs +++ b/common/nymnoise/src/lib.rs @@ -38,7 +38,9 @@ async fn upgrade_noise_initiator_v1( let secret_hash = generate_psk_v1(remote_pub_key); let noise_stream = NoiseStream::new_initiator(conn, config, remote_pub_key, &secret_hash)?; - Ok(Connection::Noise(noise_stream.perform_handshake().await?)) + Ok(Connection::Noise( + tokio::time::timeout(config.timeout, noise_stream.perform_handshake()).await??, + )) } pub async fn upgrade_noise_initiator( @@ -84,7 +86,9 @@ async fn upgrade_noise_responder_v1( let secret_hash = generate_psk_v1(config.local_key.public_key()); let noise_stream = NoiseStream::new_responder(conn, config, &secret_hash)?; - Ok(Connection::Noise(noise_stream.perform_handshake().await?)) + Ok(Connection::Noise( + tokio::time::timeout(config.timeout, noise_stream.perform_handshake()).await??, + )) } pub async fn upgrade_noise_responder( diff --git a/common/nymnoise/src/stream.rs b/common/nymnoise/src/stream.rs index 500bf64ee3d..55f4f92fe6d 100644 --- a/common/nymnoise/src/stream.rs +++ b/common/nymnoise/src/stream.rs @@ -17,8 +17,8 @@ use tokio::{ }; use tokio_util::codec::{Framed, LengthDelimitedCodec}; -const MAXMSGLEN: usize = 65535; const TAGLEN: usize = 16; +const HANDSHAKE_MAX_LEN: usize = 1024; // using this constant to limit the handshake's buffer size pub(crate) type Psk = [u8; 32]; @@ -66,7 +66,7 @@ impl NoiseStream { .new_framed(inner_stream), handshake: Some(handshake), noise: None, - dec_buffer: BytesMut::with_capacity(MAXMSGLEN), + dec_buffer: BytesMut::new(), } } @@ -92,7 +92,7 @@ impl NoiseStream { &mut self, handshake: &mut HandshakeState, ) -> Result<(), NoiseError> { - let mut buf = BytesMut::zeroed(MAXMSGLEN + TAGLEN); + let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN); // we're in the handshake, we can afford a smaller buffer let len = handshake.write_message(&[], &mut buf)?; buf.truncate(len); self.inner_stream.send(buf.into()).await?; @@ -105,7 +105,7 @@ impl NoiseStream { ) -> Result<(), NoiseError> { match self.inner_stream.next().await { Some(Ok(msg)) => { - let mut buf = vec![0u8; MAXMSGLEN]; + let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN); // we're in the handshake, we can afford a smaller buffer handshake.read_message(&msg, &mut buf)?; Ok(()) } @@ -136,7 +136,7 @@ impl AsyncRead for NoiseStream { Poll::Ready(Some(Ok(noise_msg))) => { // We have a new noise msg - let mut dec_msg = vec![0u8; MAXMSGLEN]; + let mut dec_msg = BytesMut::zeroed(noise_msg.len() - TAGLEN); let len = match projected_self.noise { Some(transport_state) => { match transport_state.read_message(&noise_msg, &mut dec_msg) { @@ -187,7 +187,7 @@ impl AsyncWrite for NoiseStream { ready!(projected_self.inner_stream.as_mut().poll_ready(cx))?; // Ready to send, encrypting message - let mut noise_buf = BytesMut::zeroed(MAXMSGLEN + TAGLEN); + let mut noise_buf = BytesMut::zeroed(buf.len() + TAGLEN); let Ok(len) = (match projected_self.noise { Some(transport_state) => transport_state.write_message(buf, &mut noise_buf), diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index d880c9125b4..2ef154c2a6d 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -1079,6 +1079,7 @@ impl NymNode { let noise_config = nym_noise::config::NoiseConfig::new( self.x25519_noise_keys.clone(), NoiseNetworkView::new_empty(), + self.config.mixnet.debug.initial_connection_timeout, ) .with_unsafe_disabled(true); @@ -1128,6 +1129,7 @@ impl NymNode { let noise_config = nym_noise::config::NoiseConfig::new( self.x25519_noise_keys.clone(), network_refresher.noise_view(), + self.config.mixnet.debug.initial_connection_timeout, ) .with_unsafe_disabled(self.config.mixnet.debug.unsafe_disable_noise); From 48045f79d74f769cadadd855e697c976ceab8ba9 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Tue, 20 May 2025 16:18:09 +0200 Subject: [PATCH 19/21] add multiple output for semi-skimmed endpoint --- Cargo.lock | 2 +- .../nym_nodes/handlers/unstable/semi_skimmed.rs | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 14d243eca79..ce113eacc13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6428,7 +6428,7 @@ dependencies = [ "nym-crypto", "nym-noise-keys", "pin-project", - "sha2 0.10.8", + "sha2 0.10.9", "snow", "strum 0.26.3", "thiserror 2.0.12", diff --git a/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs b/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs index 572ba25a193..77aac9c9a62 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs @@ -7,7 +7,6 @@ use crate::nym_nodes::handlers::unstable::helpers::{refreshed_at, LegacyAnnotati use crate::nym_nodes::handlers::unstable::NodesParamsWithRole; use crate::support::http::state::AppState; use axum::extract::{Query, State}; -use axum::Json; use nym_api_requests::models::{ NodeAnnotation, NymNodeDescription, OffsetDateTimeJsonSchemaWrapper, }; @@ -99,12 +98,16 @@ pub struct PaginatedCachedNodesExpandedResponseSchema { path = "", context_path = "/v1/unstable/nym-nodes/semi-skimmed", responses( - (status = 200, body = PaginatedCachedNodesExpandedResponseSchema) + (status = 200, content( + (PaginatedCachedNodesExpandedResponseSchema = "application/json"), + (PaginatedCachedNodesExpandedResponseSchema = "application/yaml"), + (PaginatedCachedNodesExpandedResponseSchema = "application/bincode") + )) ) )] pub(super) async fn nodes_expanded( state: State, - _query_params: Query, + query_params: Query, ) -> PaginatedSemiSkimmedNodes { // 1. grab all relevant described nym-nodes let rewarded_set = state.rewarded_set().await?; @@ -132,8 +135,6 @@ pub(super) async fn nodes_expanded( legacy_gateways.timestamp(), ]); - Ok(Json(PaginatedCachedNodesResponse::new_full( - refreshed_at, - nodes, - ))) + let output = query_params.output.unwrap_or_default(); + Ok(output.to_response(PaginatedCachedNodesResponse::new_full(refreshed_at, nodes))) } From a66b2ae0064645d289e1979a5b901af8fbf75ed3 Mon Sep 17 00:00:00 2001 From: benedetta davico <46782255+benedettadavico@users.noreply.github.com> Date: Tue, 20 May 2025 16:33:19 +0200 Subject: [PATCH 20/21] Bump ns-api version From 8125c0ab42ec44c774b02085ea446d39bd34ddaf Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Wed, 21 May 2025 09:54:20 +0200 Subject: [PATCH 21/21] backwards compatibility for mixnodes announced keys --- nym-api/nym-api-requests/src/models.rs | 10 +- nym-api/src/nym_nodes/handlers/mod.rs | 2 +- nym-node/nym-node-requests/src/api/mod.rs | 106 ++++++++++++++++-- .../src/api/v1/node/models.rs | 54 ++++++++- nym-node/src/node/http/helpers/mod.rs | 8 +- 5 files changed, 158 insertions(+), 22 deletions(-) diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index 8109b23f45d..27a03f32c3c 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -887,7 +887,7 @@ pub struct HostKeys { pub x25519: x25519::PublicKey, #[serde(default)] - pub x25519_noise: Option, + pub x25519_versioned_noise: Option, } impl From for HostKeys { @@ -895,7 +895,7 @@ impl From for HostKeys { HostKeys { ed25519: value.ed25519_identity, x25519: value.x25519_sphinx, - x25519_noise: value.x25519_noise, + x25519_versioned_noise: value.x25519_versioned_noise, } } } @@ -1050,7 +1050,11 @@ impl NymNodeDescription { SemiSkimmedNode { basic: skimmed_node, - x25519_noise_versioned_key: self.description.host_information.keys.x25519_noise, + x25519_noise_versioned_key: self + .description + .host_information + .keys + .x25519_versioned_noise, } } } diff --git a/nym-api/src/nym_nodes/handlers/mod.rs b/nym-api/src/nym_nodes/handlers/mod.rs index 1420e490011..60b8b51a982 100644 --- a/nym-api/src/nym_nodes/handlers/mod.rs +++ b/nym-api/src/nym_nodes/handlers/mod.rs @@ -170,7 +170,7 @@ async fn nodes_noise( n.description .host_information .keys - .x25519_noise + .x25519_versioned_noise .map(|noise_key| (noise_key, n)) }) .map(|(noise_key, node)| NoiseDetails { diff --git a/nym-node/nym-node-requests/src/api/mod.rs b/nym-node/nym-node-requests/src/api/mod.rs index 5e630feb70d..c0797304e89 100644 --- a/nym-node/nym-node-requests/src/api/mod.rs +++ b/nym-node/nym-node-requests/src/api/mod.rs @@ -8,6 +8,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::ops::Deref; +use v1::node::models::LegacyHostInformationV3; #[cfg(feature = "client")] pub mod client; @@ -68,9 +69,18 @@ impl SignedHostInformation { // TODO: @JS: to remove downgrade support in future release(s) + let legacy_v3 = SignedData { + data: LegacyHostInformationV3::from(self.data.clone()), + signature: self.signature.clone(), + }; + + if legacy_v3.verify(&self.keys.ed25519_identity) { + return true; + } + // attempt to verify legacy signatures let legacy_v2 = SignedData { - data: LegacyHostInformationV2::from(self.data.clone()), + data: LegacyHostInformationV2::from(legacy_v3.data), signature: self.signature.clone(), }; @@ -119,7 +129,7 @@ mod tests { let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); let ed22519 = ed25519::KeyPair::new(&mut rng); let x25519_sphinx = x25519::KeyPair::new(&mut rng); - let x25519_noise = VersionedNoiseKey { + let x25519_versioned_noise = VersionedNoiseKey { version: NoiseVersion::V1, x25519_pubkey: *x25519::KeyPair::new(&mut rng).public_key(), }; @@ -130,7 +140,7 @@ mod tests { keys: crate::api::v1::node::models::HostKeys { ed25519_identity: *ed22519.public_key(), x25519_sphinx: *x25519_sphinx.public_key(), - x25519_noise: None, + x25519_versioned_noise: None, }, }; @@ -144,7 +154,7 @@ mod tests { keys: crate::api::v1::node::models::HostKeys { ed25519_identity: *ed22519.public_key(), x25519_sphinx: *x25519_sphinx.public_key(), - x25519_noise: Some(x25519_noise), + x25519_versioned_noise: Some(x25519_versioned_noise), }, }; @@ -154,6 +164,84 @@ mod tests { assert!(signed_info.verify_host_information()); } + #[test] + fn dummy_legacy_v3_signed_host_verification() { + let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); + let ed22519 = ed25519::KeyPair::new(&mut rng); + let x25519_sphinx = x25519::KeyPair::new(&mut rng); + let x25519_noise = x25519::KeyPair::new(&mut rng); + + let legacy_info_no_noise = crate::api::v1::node::models::LegacyHostInformationV3 { + ip_address: vec!["1.1.1.1".parse().unwrap()], + hostname: Some("foomp.com".to_string()), + keys: crate::api::v1::node::models::LegacyHostKeysV3 { + ed25519_identity: *ed22519.public_key(), + x25519_sphinx: *x25519_sphinx.public_key(), + x25519_noise: None, + }, + }; + + //technically this variant should never happen + let legacy_info_noise = crate::api::v1::node::models::LegacyHostInformationV3 { + ip_address: vec!["1.1.1.1".parse().unwrap()], + hostname: Some("foomp.com".to_string()), + keys: crate::api::v1::node::models::LegacyHostKeysV3 { + ed25519_identity: *ed22519.public_key(), + x25519_sphinx: *x25519_sphinx.public_key(), + x25519_noise: Some(*x25519_noise.public_key()), + }, + }; + + let host_info_no_noise = crate::api::v1::node::models::HostInformation { + ip_address: legacy_info_no_noise.ip_address.clone(), + hostname: legacy_info_no_noise.hostname.clone(), + keys: crate::api::v1::node::models::HostKeys { + ed25519_identity: legacy_info_no_noise.keys.ed25519_identity, + x25519_sphinx: legacy_info_no_noise.keys.x25519_sphinx, + x25519_versioned_noise: None, + }, + }; + + let host_info_noise = crate::api::v1::node::models::HostInformation { + ip_address: legacy_info_noise.ip_address.clone(), + hostname: legacy_info_noise.hostname.clone(), + keys: crate::api::v1::node::models::HostKeys { + ed25519_identity: legacy_info_noise.keys.ed25519_identity, + x25519_sphinx: legacy_info_noise.keys.x25519_sphinx, + x25519_versioned_noise: Some(VersionedNoiseKey { + version: NoiseVersion::V1, + x25519_pubkey: legacy_info_noise.keys.x25519_noise.unwrap(), + }), + }, + }; + + // signature on legacy data + let signature_no_noise = SignedData::new(legacy_info_no_noise, ed22519.private_key()) + .unwrap() + .signature; + + let signature_noise = SignedData::new(legacy_info_noise, ed22519.private_key()) + .unwrap() + .signature; + + // signed blob with the 'current' structure + let current_struct_no_noise = SignedData { + data: host_info_no_noise, + signature: signature_no_noise, + }; + + let current_struct_noise = SignedData { + data: host_info_noise, + signature: signature_noise, + }; + + assert!(!current_struct_no_noise.verify(ed22519.public_key())); + assert!(current_struct_no_noise.verify_host_information()); + + assert!(!current_struct_noise.verify(ed22519.public_key())); + assert!(current_struct_noise.verify_host_information()) + } + #[test] fn dummy_legacy_v2_signed_host_verification() { let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); @@ -187,7 +275,7 @@ mod tests { keys: crate::api::v1::node::models::HostKeys { ed25519_identity: legacy_info_no_noise.keys.ed25519_identity.parse().unwrap(), x25519_sphinx: legacy_info_no_noise.keys.x25519_sphinx.parse().unwrap(), - x25519_noise: None, + x25519_versioned_noise: None, }, }; @@ -197,7 +285,7 @@ mod tests { keys: crate::api::v1::node::models::HostKeys { ed25519_identity: legacy_info_noise.keys.ed25519_identity.parse().unwrap(), x25519_sphinx: legacy_info_noise.keys.x25519_sphinx.parse().unwrap(), - x25519_noise: Some(VersionedNoiseKey { + x25519_versioned_noise: Some(VersionedNoiseKey { version: NoiseVersion::V1, x25519_pubkey: legacy_info_noise.keys.x25519_noise.parse().unwrap(), }), @@ -244,7 +332,7 @@ mod tests { keys: crate::api::v1::node::models::HostKeys { ed25519_identity: *ed22519.public_key(), x25519_sphinx: *x25519_sphinx.public_key(), - x25519_noise: None, + x25519_versioned_noise: None, }, }; @@ -254,7 +342,7 @@ mod tests { keys: crate::api::v1::node::models::HostKeys { ed25519_identity: *ed22519.public_key(), x25519_sphinx: *x25519_sphinx.public_key(), - x25519_noise: Some(VersionedNoiseKey { + x25519_versioned_noise: Some(VersionedNoiseKey { version: NoiseVersion::V1, x25519_pubkey: *x25519_noise.public_key(), }), @@ -295,7 +383,7 @@ mod tests { keys: crate::api::v1::node::models::HostKeys { ed25519_identity: legacy_info.keys.ed25519.parse().unwrap(), x25519_sphinx: legacy_info.keys.x25519.parse().unwrap(), - x25519_noise: None, + x25519_versioned_noise: None, }, }; diff --git a/nym-node/nym-node-requests/src/api/v1/node/models.rs b/nym-node/nym-node-requests/src/api/v1/node/models.rs index 04655e3f886..3c2255dea02 100644 --- a/nym-node/nym-node-requests/src/api/v1/node/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/node/models.rs @@ -3,7 +3,9 @@ use celes::Country; use nym_crypto::asymmetric::ed25519::{self, serde_helpers::bs58_ed25519_pubkey}; -use nym_crypto::asymmetric::x25519::{self, serde_helpers::bs58_x25519_pubkey}; +use nym_crypto::asymmetric::x25519::{ + self, serde_helpers::bs58_x25519_pubkey, serde_helpers::option_bs58_x25519_pubkey, +}; use nym_noise_keys::VersionedNoiseKey; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -68,6 +70,13 @@ impl HostInformation { } } +#[derive(Serialize)] +pub struct LegacyHostInformationV3 { + pub ip_address: Vec, + pub hostname: Option, + pub keys: LegacyHostKeysV3, +} + #[derive(Serialize)] pub struct LegacyHostInformationV2 { pub ip_address: Vec, @@ -82,8 +91,18 @@ pub struct LegacyHostInformation { pub keys: LegacyHostKeys, } -impl From for LegacyHostInformationV2 { +impl From for LegacyHostInformationV3 { fn from(value: HostInformation) -> Self { + LegacyHostInformationV3 { + ip_address: value.ip_address, + hostname: value.hostname, + keys: value.keys.into(), + } + } +} + +impl From for LegacyHostInformationV2 { + fn from(value: LegacyHostInformationV3) -> Self { LegacyHostInformationV2 { ip_address: value.ip_address, hostname: value.hostname, @@ -122,7 +141,22 @@ pub struct HostKeys { /// Base58-encoded x25519 public key of this node used for the noise protocol. #[serde(default)] - pub x25519_noise: Option, + pub x25519_versioned_noise: Option, +} + +#[derive(Serialize)] +pub struct LegacyHostKeysV3 { + #[serde(alias = "ed25519")] + #[serde(with = "bs58_ed25519_pubkey")] + pub ed25519_identity: ed25519::PublicKey, + + #[serde(alias = "x25519")] + #[serde(with = "bs58_x25519_pubkey")] + pub x25519_sphinx: x25519::PublicKey, + + #[serde(default)] + #[serde(with = "option_bs58_x25519_pubkey")] + pub x25519_noise: Option, } #[derive(Serialize)] @@ -138,14 +172,24 @@ pub struct LegacyHostKeys { pub x25519: String, } -impl From for LegacyHostKeysV2 { +impl From for LegacyHostKeysV3 { fn from(value: HostKeys) -> Self { + LegacyHostKeysV3 { + ed25519_identity: value.ed25519_identity, + x25519_sphinx: value.x25519_sphinx, + x25519_noise: value.x25519_versioned_noise.map(|k| k.x25519_pubkey), + } + } +} + +impl From for LegacyHostKeysV2 { + fn from(value: LegacyHostKeysV3) -> Self { LegacyHostKeysV2 { ed25519_identity: value.ed25519_identity.to_base58_string(), x25519_sphinx: value.x25519_sphinx.to_base58_string(), x25519_noise: value .x25519_noise - .map(|k| k.x25519_pubkey.to_base58_string()) + .map(|k| k.to_base58_string()) .unwrap_or_default(), } } diff --git a/nym-node/src/node/http/helpers/mod.rs b/nym-node/src/node/http/helpers/mod.rs index 04114832e1a..8e98f4d8033 100644 --- a/nym-node/src/node/http/helpers/mod.rs +++ b/nym-node/src/node/http/helpers/mod.rs @@ -14,13 +14,13 @@ pub mod system_info; pub(crate) fn sign_host_details( config: &Config, x22519_sphinx: &x25519::PublicKey, - x25519_noise: &VersionedNoiseKey, + x25519_versioned_noise: &VersionedNoiseKey, ed22519_identity: &ed25519::KeyPair, ) -> Result { - let x25519_noise = if config.mixnet.debug.unsafe_disable_noise { + let x25519_versioned_noise = if config.mixnet.debug.unsafe_disable_noise { None } else { - Some(*x25519_noise) + Some(*x25519_versioned_noise) }; let host_info = api_requests::v1::node::models::HostInformation { @@ -29,7 +29,7 @@ pub(crate) fn sign_host_details( keys: api_requests::v1::node::models::HostKeys { ed25519_identity: *ed22519_identity.public_key(), x25519_sphinx: *x22519_sphinx, - x25519_noise, + x25519_versioned_noise, }, };