diff --git a/Cargo.lock b/Cargo.lock index 7a0e2e0a12..21059ac3ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4781,6 +4781,7 @@ dependencies = [ "bincode", "bytes", "nym-bin-common", + "nym-crypto", "nym-sphinx", "rand 0.8.5", "serde", diff --git a/common/ip-packet-requests/Cargo.toml b/common/ip-packet-requests/Cargo.toml index 847d6a05bc..6a7eb57ae0 100644 --- a/common/ip-packet-requests/Cargo.toml +++ b/common/ip-packet-requests/Cargo.toml @@ -12,6 +12,7 @@ license.workspace = true bincode = { workspace = true } bytes = { workspace = true } nym-bin-common = { path = "../bin-common" } +nym-crypto = { path = "../crypto" } nym-sphinx = { path = "../nymsphinx" } rand = "0.8.5" serde = { workspace = true, features = ["derive"] } diff --git a/common/ip-packet-requests/src/v7/mod.rs b/common/ip-packet-requests/src/v7/mod.rs index 70d9e9d7d7..7279419b76 100644 --- a/common/ip-packet-requests/src/v7/mod.rs +++ b/common/ip-packet-requests/src/v7/mod.rs @@ -1,5 +1,6 @@ pub mod conversion; pub mod request; pub mod response; +pub mod signature; const VERSION: u8 = 7; diff --git a/common/ip-packet-requests/src/v7/request.rs b/common/ip-packet-requests/src/v7/request.rs index 87aa8610d7..9f76f77ec0 100644 --- a/common/ip-packet-requests/src/v7/request.rs +++ b/common/ip-packet-requests/src/v7/request.rs @@ -1,10 +1,14 @@ +use nym_crypto::asymmetric::identity; use nym_sphinx::addressing::clients::Recipient; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use crate::{make_bincode_serializer, IpPair}; -use super::VERSION; +use super::{ + signature::{SignatureError, SignedRequest}, + VERSION, +}; fn generate_random() -> u64 { use rand::RngCore; @@ -236,6 +240,25 @@ pub struct SignedStaticConnectRequest { pub signature: Option>, } +impl SignedRequest for SignedStaticConnectRequest { + fn identity(&self) -> &identity::PublicKey { + self.request.reply_to.identity() + } + + fn request(&self) -> Result, SignatureError> { + self.request + .to_bytes() + .map_err(|error| SignatureError::RequestSerializationError { + message: "failed to serialize request to binary".to_string(), + error, + }) + } + + fn signature(&self) -> Option<&Vec> { + self.signature.as_ref() + } +} + // A dynamic connect request is when the client does not provide the internal IP address it will use // on the ip packet router, and instead requests one to be assigned to it. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] @@ -274,6 +297,25 @@ pub struct SignedDynamicConnectRequest { pub signature: Option>, } +impl SignedRequest for SignedDynamicConnectRequest { + fn identity(&self) -> &identity::PublicKey { + self.request.reply_to.identity() + } + + fn request(&self) -> Result, SignatureError> { + self.request + .to_bytes() + .map_err(|error| SignatureError::RequestSerializationError { + message: "failed to serialize request to binary".to_string(), + error, + }) + } + + fn signature(&self) -> Option<&Vec> { + self.signature.as_ref() + } +} + // A disconnect request is when the client wants to disconnect from the ip packet router and free // up the allocated IP address. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] @@ -300,6 +342,25 @@ pub struct SignedDisconnectRequest { pub signature: Option>, } +impl SignedRequest for SignedDisconnectRequest { + fn identity(&self) -> &identity::PublicKey { + self.request.reply_to.identity() + } + + fn request(&self) -> Result, SignatureError> { + self.request + .to_bytes() + .map_err(|error| SignatureError::RequestSerializationError { + message: "failed to serialize request to binary".to_string(), + error, + }) + } + + fn signature(&self) -> Option<&Vec> { + self.signature.as_ref() + } +} + // A data request is when the client wants to send an IP packet to a destination. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct DataRequest { diff --git a/common/ip-packet-requests/src/v7/signature.rs b/common/ip-packet-requests/src/v7/signature.rs new file mode 100644 index 0000000000..b22ec242f3 --- /dev/null +++ b/common/ip-packet-requests/src/v7/signature.rs @@ -0,0 +1,54 @@ +use nym_crypto::asymmetric::{ed25519::Ed25519RecoveryError, identity}; + +#[derive(thiserror::Error, Debug)] +pub enum SignatureError { + #[error("signature is missing")] + MissingSignature, + + #[error("failed to parse signature: {error}")] + SignatureParseError { + message: String, + error: Ed25519RecoveryError, + }, + + #[error("failed to serialize request to binary: {message}")] + RequestSerializationError { + message: String, + error: Box, + }, + + #[error("signature verification failed")] + VerificationFailed { + message: String, + error: identity::SignatureError, + }, +} + +pub trait SignedRequest { + fn identity(&self) -> &identity::PublicKey; + + fn request(&self) -> Result, SignatureError>; + + fn signature(&self) -> Option<&Vec>; + + fn verify(&self) -> Result<(), SignatureError> { + if let Some(signature) = self.signature() { + let request_as_bytes = self.request()?; + let signature = identity::Signature::from_bytes(signature).map_err(|error| { + SignatureError::SignatureParseError { + message: "failed to parse signature".to_string(), + error, + } + })?; + + self.identity() + .verify(request_as_bytes, &signature) + .map_err(|error| SignatureError::VerificationFailed { + message: "signature verification failed".to_string(), + error, + }) + } else { + Err(SignatureError::MissingSignature) + } + } +} diff --git a/service-providers/ip-packet-router/src/error.rs b/service-providers/ip-packet-router/src/error.rs index c9771c3e42..ff9f85cff5 100644 --- a/service-providers/ip-packet-router/src/error.rs +++ b/service-providers/ip-packet-router/src/error.rs @@ -91,6 +91,11 @@ pub enum IpPacketRouterError { #[error("received empty packet")] EmptyPacket, + + #[error("failed to verify request: {source}")] + FailedToVerifyRequest { + source: nym_ip_packet_requests::v7::signature::SignatureError, + }, } pub type Result = std::result::Result; diff --git a/service-providers/ip-packet-router/src/mixnet_listener.rs b/service-providers/ip-packet-router/src/mixnet_listener.rs index 11cd8fbe53..d087eff38f 100644 --- a/service-providers/ip-packet-router/src/mixnet_listener.rs +++ b/service-providers/ip-packet-router/src/mixnet_listener.rs @@ -4,16 +4,19 @@ use std::{collections::HashMap, net::SocketAddr}; use bytes::{Bytes, BytesMut}; use futures::StreamExt; -use nym_ip_packet_requests::v7::request::IpPacketRequestData; +use nym_ip_packet_requests::v7::signature::SignatureError; use nym_ip_packet_requests::{ codec::MultiIpPacketCodec, v6::response::{ DynamicConnectFailureReason, InfoLevel, InfoResponseReply, IpPacketResponse, StaticConnectFailureReason, }, - v7::request::{ - DataRequest, DisconnectRequest, DynamicConnectRequest, IpPacketRequest, - StaticConnectRequest, + v7::{ + request::{ + DataRequest, DisconnectRequest, DynamicConnectRequest, IpPacketRequest, + IpPacketRequestData, StaticConnectRequest, + }, + signature::SignedRequest, }, IpPair, }; @@ -543,67 +546,59 @@ impl MixnetListener { reconstructed.sender_tag ); - // Check version of request - // let version = if let Some(version) = reconstructed.message.first() { - // // The idea is that in the future we can add logic here to parse older versions to stay - // // backwards compatible. - // if *version != nym_ip_packet_requests::CURRENT_VERSION - // && *version != nym_ip_packet_requests::CURRENT_VERSION + 1 - // { - // log::info!("Received packet with invalid version: v{version}"); - // return Ok(vec![self.on_version_mismatch(*version, &reconstructed)]); - // } - // *version - // }; - - // Make sure the current version is set to something that we can handle - const _: () = assert!( - nym_ip_packet_requests::CURRENT_VERSION == 6 - || nym_ip_packet_requests::CURRENT_VERSION == 7 - ); - - let version = *reconstructed + let request_version = *reconstructed .message .first() .ok_or(IpPacketRouterError::EmptyPacket)?; - // Check version of request - let request = if version == 6 { - nym_ip_packet_requests::v6::request::IpPacketRequest::from_reconstructed_message( + // Check version of the request and convert to the latest version if necessary + let request = match request_version { + 6 => nym_ip_packet_requests::v6::request::IpPacketRequest::from_reconstructed_message( &reconstructed, ) .map_err(|err| IpPacketRouterError::FailedToDeserializeTaggedPacket { source: err })? - .into() - } else if version == 7 { - nym_ip_packet_requests::v7::request::IpPacketRequest::from_reconstructed_message( + .into(), + 7 => nym_ip_packet_requests::v7::request::IpPacketRequest::from_reconstructed_message( &reconstructed, ) - .map_err(|err| IpPacketRouterError::FailedToDeserializeTaggedPacket { source: err })? - } else { - log::info!("Received packet with invalid version: v{version}"); - return Ok(vec![self.on_version_mismatch(version, &reconstructed)]); + .map_err(|err| IpPacketRouterError::FailedToDeserializeTaggedPacket { source: err })?, + _ => { + log::info!("Received packet with invalid version: v{request_version}"); + return Ok(vec![ + self.on_version_mismatch(request_version, &reconstructed) + ]); + } }; + // Once we start to require clients to send v7 requests, we will enfore checking + // signatures. Until then, we only check if they are present. + match request.data { IpPacketRequestData::StaticConnect(signed_connect_request) => { - if let Some(_signature) = signed_connect_request.signature { - // TODO: verify the signature + if let Err(err) = signed_connect_request.verify() { + if !matches!(err, SignatureError::MissingSignature) { + return Err(IpPacketRouterError::FailedToVerifyRequest { source: err }); + } } let connect_request = signed_connect_request.request; Ok(vec![self.on_static_connect_request(connect_request).await]) } IpPacketRequestData::DynamicConnect(signed_connect_request) => { - if let Some(_signature) = signed_connect_request.signature { - // TODO: verify the signature + if let Err(err) = signed_connect_request.verify() { + if !matches!(err, SignatureError::MissingSignature) { + return Err(IpPacketRouterError::FailedToVerifyRequest { source: err }); + } } let connect_request = signed_connect_request.request; Ok(vec![self.on_dynamic_connect_request(connect_request).await]) } - IpPacketRequestData::Disconnect(disconnect_request) => { - if let Some(_signature) = disconnect_request.signature { - // TODO: verify the signature + IpPacketRequestData::Disconnect(signed_disconnect_request) => { + if let Err(err) = signed_disconnect_request.verify() { + if !matches!(err, SignatureError::MissingSignature) { + return Err(IpPacketRouterError::FailedToVerifyRequest { source: err }); + } } - let disconnect_request = disconnect_request.request; + let disconnect_request = signed_disconnect_request.request; Ok(vec![self.on_disconnect_request(disconnect_request)]) } IpPacketRequestData::Data(data_request) => self.on_data_request(data_request).await,