diff --git a/Cargo.lock b/Cargo.lock index 793b68e499..d600caf21b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2717,7 +2717,7 @@ dependencies = [ "ed25519 2.2.2", "rand_core 0.6.4", "serde", - "sha2 0.10.7", + "sha2 0.10.8", "zeroize", ] @@ -3871,6 +3871,9 @@ dependencies = [ "serde", "serde_json", "thiserror", + "tracing", + "url", + "wasmtimer", ] [[package]] @@ -3884,6 +3887,12 @@ dependencies = [ "pin-project-lite 0.2.12", ] +[[package]] +name = "http-range-header" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" + [[package]] name = "httparse" version = "1.8.0" @@ -4135,6 +4144,7 @@ checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" dependencies = [ "equivalent", "hashbrown 0.14.0", + "serde", ] [[package]] @@ -4270,7 +4280,7 @@ dependencies = [ "socket2 0.4.9", "widestring", "winapi", - "winreg", + "winreg 0.10.1", ] [[package]] @@ -5995,6 +6005,7 @@ dependencies = [ "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-name-service-common", + "nym-node-requests", "nym-node-tester-utils", "nym-pemstore", "nym-service-provider-directory-common", @@ -6005,7 +6016,6 @@ dependencies = [ "nym-vesting-contract-common", "okapi", "pin-project", - "pretty_env_logger", "rand 0.7.3", "rand 0.8.5", "reqwest", @@ -6039,6 +6049,7 @@ dependencies = [ "getset", "nym-coconut-interface", "nym-mixnet-contract-common", + "nym-node-requests", "schemars", "serde", "ts-rs", @@ -6079,6 +6090,7 @@ dependencies = [ "tracing-opentelemetry", "tracing-subscriber", "tracing-tree", + "utoipa", "vergen", ] @@ -6535,6 +6547,7 @@ dependencies = [ "nym-mixnode-common", "nym-network-defaults", "nym-network-requester", + "nym-node", "nym-pemstore", "nym-sphinx", "nym-statistics-common", @@ -6850,6 +6863,52 @@ dependencies = [ "tokio", ] +[[package]] +name = "nym-node" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "base64 0.21.4", + "bytes", + "colored", + "fastrand 2.0.0", + "hmac 0.12.1", + "hyper", + "mime", + "nym-crypto", + "nym-node-requests", + "nym-task", + "rand 0.7.3", + "serde", + "serde_json", + "serde_yaml", + "sha2 0.10.8", + "thiserror", + "tokio", + "tower", + "tower-http", + "tracing", + "utoipa", + "utoipa-swagger-ui", + "x25519-dalek 2.0.0", +] + +[[package]] +name = "nym-node-requests" +version = "0.1.0" +dependencies = [ + "async-trait", + "http-api-client", + "nym-bin-common", + "nym-crypto", + "schemars", + "serde", + "serde_json", + "thiserror", + "utoipa", +] + [[package]] name = "nym-node-tester-utils" version = "0.1.0" @@ -8815,9 +8874,9 @@ checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" [[package]] name = "reqwest" -version = "0.11.18" +version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ "base64 0.21.4", "bytes", @@ -8840,6 +8899,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "system-configuration", "tokio", "tokio-native-tls", "tokio-socks", @@ -8848,7 +8908,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", + "winreg 0.50.0", ] [[package]] @@ -9654,6 +9714,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +dependencies = [ + "indexmap 2.0.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha-1" version = "0.9.8" @@ -10900,6 +10973,31 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" +dependencies = [ + "bitflags 2.4.0", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite 0.2.12", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-layer" version = "0.3.2" @@ -11331,6 +11429,12 @@ dependencies = [ "subtle 2.4.1", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" + [[package]] name = "unsigned-varint" version = "0.7.1" @@ -11410,11 +11514,11 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "utoipa" -version = "3.3.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ae74ef183fae36d650f063ae7bde1cacbe1cd7e72b617cbe1e985551878b98" +checksum = "d82b1bc5417102a73e8464c686eef947bdfb99fcdfc0a4f228e81afa9526470a" dependencies = [ - "indexmap 1.9.3", + "indexmap 2.0.0", "serde", "serde_json", "utoipa-gen", @@ -11422,11 +11526,10 @@ dependencies = [ [[package]] name = "utoipa-gen" -version = "3.3.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ea8ac818da7e746a63285594cce8a96f5e00ee31994e655bd827569cb8b137b" +checksum = "05d96dcd6fc96f3df9b3280ef480770af1b7c5d14bc55192baa9b067976d920c" dependencies = [ - "lazy_static", "proc-macro-error", "proc-macro2", "quote", @@ -11436,11 +11539,12 @@ dependencies = [ [[package]] name = "utoipa-swagger-ui" -version = "3.1.3" +version = "3.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "062bba5a3568e126ac72049a63254f4cb1da2eb713db0c1ab2a4c76be191db8c" +checksum = "84614caa239fb25b2bb373a52859ffd94605ceb256eeb1d63436325cf81e3653" dependencies = [ "actix-web", + "axum", "mime_guess", "regex", "rust-embed", @@ -12251,6 +12355,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "with_builtin_macros" version = "0.0.3" diff --git a/gateway/src/http/mod.rs b/gateway/src/http/mod.rs index b379245955..55861801d7 100644 --- a/gateway/src/http/mod.rs +++ b/gateway/src/http/mod.rs @@ -3,15 +3,18 @@ use crate::config::Config; use crate::error::GatewayError; -use crate::node::helpers::{load_identity_keys, load_public_key}; +use crate::node::helpers::load_public_key; use nym_bin_common::bin_info_owned; use nym_crypto::asymmetric::{encryption, identity}; use nym_node::error::NymNodeError; use nym_node::http::api::api_requests; use nym_node::http::api::api_requests::SignedHostInformation; +use nym_node::http::router::WireguardAppState; +use nym_node::wireguard::types::ClientRegistry; use nym_sphinx::addressing::clients::Recipient; use nym_task::TaskClient; -use zeroize::Zeroize; +use std::sync::Arc; +use tokio::sync::RwLock; fn load_gateway_details( config: &Config, @@ -29,27 +32,21 @@ fn load_gateway_details( fn load_host_details( config: &Config, + sphinx_key: &encryption::PublicKey, + identity_keypair: &identity::KeyPair, ) -> Result { - let mut identity_keypair = load_identity_keys(config)?; - - let sphinx_public_key: encryption::PublicKey = load_public_key( - &config.storage_paths.keys.public_sphinx_key_file, - "gateway sphinx", - )?; - let host_info = api_requests::v1::node::models::HostInformation { // TODO: this should be extracted differently, i.e. it's the issue of the public/private address ip_address: vec![config.gateway.listening_address.to_string()], hostname: None, keys: api_requests::v1::node::models::HostKeys { ed25519: identity_keypair.public_key().to_base58_string(), - x25519: sphinx_public_key.to_base58_string(), + x25519: sphinx_key.to_base58_string(), }, }; let signed_info = SignedHostInformation::new(host_info, identity_keypair.private_key()) .map_err(NymNodeError::from)?; - identity_keypair.zeroize(); Ok(signed_info) } @@ -97,6 +94,11 @@ fn load_network_requester_details( pub(crate) fn start_http_api( gateway_config: &Config, network_requester_config: Option<&nym_network_requester::Config>, + client_registry: Arc>, + identity_keypair: &identity::KeyPair, + // TODO: this should be a wg specific key and not re-used sphinx + sphinx_keypair: Arc, + task_client: TaskClient, ) -> Result<(), GatewayError> { // is it suboptimal to load all the keys, etc for the second time after they've already been @@ -105,18 +107,23 @@ pub(crate) fn start_http_api( // but in the grand scheme of things performance penalty is negligible since it's only happening on startup // and makes the code a bit nicer to manage. on top of it, all of it will refactored anyway at some point // (famous last words, eh? - 22.09.23) - - // TODO: load private key, set zeroizing wrapper and sign whatever responses we need to sign - let mut config = - nym_node::http::Config::new(bin_info_owned!(), load_host_details(gateway_config)?) - .with_gateway(load_gateway_details(gateway_config)?); + let mut config = nym_node::http::Config::new( + bin_info_owned!(), + load_host_details( + gateway_config, + sphinx_keypair.public_key(), + identity_keypair, + )?, + ) + .with_gateway(load_gateway_details(gateway_config)?); if let Some(nr_config) = network_requester_config { config = config .with_network_requester(load_network_requester_details(gateway_config, nr_config)?) } - let router = nym_node::http::NymNodeRouter::new(config); + let wg_state = WireguardAppState::new(sphinx_keypair, client_registry, Default::default()); + let router = nym_node::http::NymNodeRouter::new(config, Some(wg_state)); let server = router .build_server(&gateway_config.http.bind_address)? diff --git a/gateway/src/node/client_handling/client_registration.rs b/gateway/src/node/client_handling/client_registration.rs index b8fa5a1cbe..2c74c26c6c 100644 --- a/gateway/src/node/client_handling/client_registration.rs +++ b/gateway/src/node/client_handling/client_registration.rs @@ -1,181 +1,182 @@ -use std::{ - collections::HashMap, - fmt, - hash::{Hash, Hasher}, - net::SocketAddr, - ops::Deref, - str::FromStr, -}; - -use base64::{engine::general_purpose, Engine}; -use hmac::{Hmac, Mac}; -use nym_crypto::asymmetric::encryption::PrivateKey; -use serde::{Deserialize, Serialize}; -use sha2::Sha256; -use x25519_dalek::StaticSecret; - -use crate::error::GatewayError; - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub(crate) enum ClientMessage { - Init(InitMessage), - Final(Client), -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub(crate) struct InitMessage { - pub_key: ClientPublicKey, -} - -impl InitMessage { - pub fn pub_key(&self) -> &ClientPublicKey { - &self.pub_key - } - #[allow(dead_code)] - pub fn new(pub_key: ClientPublicKey) -> Self { - InitMessage { pub_key } - } -} - -// Client that wants to register sends its PublicKey and SocketAddr bytes mac digest encrypted with a DH shared secret. -// Gateway can then verify pub_key payload using the sme process -#[derive(Debug, Clone, Deserialize, Serialize)] -pub(crate) struct Client { - // base64 encoded public key, using x25519-dalek for impl - pub(crate) pub_key: ClientPublicKey, - pub(crate) socket: SocketAddr, - pub(crate) mac: ClientMac, -} - -pub type HmacSha256 = Hmac; - -impl Client { - // Reusable secret should be gateways Wireguard PK - // Client should perform this step when generating its payload, using its own WG PK - pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), GatewayError> { - #[allow(clippy::expect_used)] - let static_secret = - StaticSecret::try_from(gateway_key.to_bytes()).expect("This is infalliable"); - let dh = static_secret.diffie_hellman(&self.pub_key); - let mut mac = HmacSha256::new_from_slice(dh.as_bytes())?; - mac.update(self.pub_key.as_bytes()); - mac.update(self.socket.ip().to_string().as_bytes()); - mac.update(self.socket.port().to_string().as_bytes()); - mac.update(&nonce.to_le_bytes()); - Ok(mac.verify_slice(&self.mac)?) - } - - pub fn pub_key(&self) -> &ClientPublicKey { - &self.pub_key - } - - pub fn socket(&self) -> SocketAddr { - self.socket - } -} - -// This should go into nym-wireguard crate -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct ClientPublicKey(x25519_dalek::PublicKey); -#[derive(Debug, Clone)] -pub(crate) struct ClientMac(Vec); - -impl ClientMac { - #[allow(dead_code)] - pub fn new(mac: Vec) -> Self { - ClientMac(mac) - } -} - -impl Deref for ClientMac { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl fmt::Display for ClientPublicKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", general_purpose::STANDARD.encode(self.0.as_bytes())) - } -} - -impl Hash for ClientPublicKey { - fn hash(&self, state: &mut H) { - self.0.as_bytes().hash(state) - } -} - -impl FromStr for ClientMac { - type Err = String; - - fn from_str(s: &str) -> Result { - let mac_bytes: Vec = general_purpose::STANDARD - .decode(s) - .map_err(|_| "Could not base64 decode public key mac representation".to_string())?; - Ok(ClientMac(mac_bytes)) - } -} - -impl Serialize for ClientMac { - fn serialize(&self, serializer: S) -> Result { - let encoded_key = general_purpose::STANDARD.encode(self.0.clone()); - serializer.serialize_str(&encoded_key) - } -} - -impl<'de> Deserialize<'de> for ClientMac { - fn deserialize>(deserializer: D) -> Result { - let encoded_key = String::deserialize(deserializer)?; - ClientMac::from_str(&encoded_key).map_err(serde::de::Error::custom) - } -} - -impl ClientPublicKey { - #[allow(dead_code)] - pub fn new(key: x25519_dalek::PublicKey) -> Self { - ClientPublicKey(key) - } - - pub fn as_bytes(&self) -> &[u8] { - self.0.as_bytes() - } -} - -impl Deref for ClientPublicKey { - type Target = x25519_dalek::PublicKey; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl FromStr for ClientPublicKey { - type Err = String; - - fn from_str(s: &str) -> Result { - let key_bytes: [u8; 32] = general_purpose::STANDARD - .decode(s) - .map_err(|_| "Could not base64 decode public key representation".to_string())? - .try_into() - .map_err(|_| "Invalid key length".to_string())?; - Ok(ClientPublicKey(x25519_dalek::PublicKey::from(key_bytes))) - } -} - -impl Serialize for ClientPublicKey { - fn serialize(&self, serializer: S) -> Result { - let encoded_key = general_purpose::STANDARD.encode(self.0.as_bytes()); - serializer.serialize_str(&encoded_key) - } -} - -impl<'de> Deserialize<'de> for ClientPublicKey { - fn deserialize>(deserializer: D) -> Result { - let encoded_key = String::deserialize(deserializer)?; - Ok(ClientPublicKey::from_str(&encoded_key).map_err(serde::de::Error::custom))? - } -} - -pub(crate) type ClientRegistry = HashMap; +// use std::{ +// collections::HashMap, +// fmt, +// hash::{Hash, Hasher}, +// net::SocketAddr, +// ops::Deref, +// str::FromStr, +// }; +// +// use base64::{engine::general_purpose, Engine}; +// use hmac::{Hmac, Mac}; +// use nym_crypto::asymmetric::encryption::PrivateKey; +// use serde::{Deserialize, Serialize}; +// use sha2::Sha256; +// use x25519_dalek::StaticSecret; +// +// use crate::error::GatewayError; +// +// #[derive(Debug, Clone, Deserialize, Serialize)] +// pub(crate) enum ClientMessage { +// Init(InitMessage), +// Final(Client), +// } +// +// #[derive(Debug, Clone, Deserialize, Serialize)] +// pub(crate) struct InitMessage { +// pub_key: ClientPublicKey, +// } +// +// impl InitMessage { +// pub fn pub_key(&self) -> &ClientPublicKey { +// &self.pub_key +// } +// #[allow(dead_code)] +// pub fn new(pub_key: ClientPublicKey) -> Self { +// InitMessage { pub_key } +// } +// } +// +// // Client that wants to register sends its PublicKey and SocketAddr bytes mac digest encrypted with a DH shared secret. +// // Gateway can then verify pub_key payload using the sme process +// #[derive(Debug, Clone, Deserialize, Serialize)] +// pub(crate) struct Client { +// // base64 encoded public key, using x25519-dalek for impl +// pub(crate) pub_key: ClientPublicKey, +// pub(crate) socket: SocketAddr, +// pub(crate) mac: ClientMac, +// } +// +// pub type HmacSha256 = Hmac; +// +// impl Client { +// // Reusable secret should be gateways Wireguard PK +// // Client should perform this step when generating its payload, using its own WG PK +// pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), GatewayError> { +// #[allow(clippy::expect_used)] +// let static_secret = +// StaticSecret::try_from(gateway_key.to_bytes()).expect("This is infalliable"); +// let dh = static_secret.diffie_hellman(&self.pub_key); +// let mut mac = HmacSha256::new_from_slice(dh.as_bytes())?; +// mac.update(self.pub_key.as_bytes()); +// mac.update(self.socket.ip().to_string().as_bytes()); +// mac.update(self.socket.port().to_string().as_bytes()); +// mac.update(&nonce.to_le_bytes()); +// Ok(mac.verify_slice(&self.mac)?) +// } +// +// pub fn pub_key(&self) -> &ClientPublicKey { +// &self.pub_key +// } +// +// pub fn socket(&self) -> SocketAddr { +// self.socket +// } +// } +// +// // This should go into nym-wireguard crate +// #[derive(Debug, Clone, Eq, PartialEq)] +// pub struct ClientPublicKey(x25519_dalek::PublicKey); +// +// #[derive(Debug, Clone)] +// pub(crate) struct ClientMac(Vec); +// +// impl ClientMac { +// #[allow(dead_code)] +// pub fn new(mac: Vec) -> Self { +// ClientMac(mac) +// } +// } +// +// impl Deref for ClientMac { +// type Target = Vec; +// +// fn deref(&self) -> &Self::Target { +// &self.0 +// } +// } +// +// impl fmt::Display for ClientPublicKey { +// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +// write!(f, "{}", general_purpose::STANDARD.encode(self.0.as_bytes())) +// } +// } +// +// impl Hash for ClientPublicKey { +// fn hash(&self, state: &mut H) { +// self.0.as_bytes().hash(state) +// } +// } +// +// impl FromStr for ClientMac { +// type Err = String; +// +// fn from_str(s: &str) -> Result { +// let mac_bytes: Vec = general_purpose::STANDARD +// .decode(s) +// .map_err(|_| "Could not base64 decode public key mac representation".to_string())?; +// Ok(ClientMac(mac_bytes)) +// } +// } +// +// impl Serialize for ClientMac { +// fn serialize(&self, serializer: S) -> Result { +// let encoded_key = general_purpose::STANDARD.encode(self.0.clone()); +// serializer.serialize_str(&encoded_key) +// } +// } +// +// impl<'de> Deserialize<'de> for ClientMac { +// fn deserialize>(deserializer: D) -> Result { +// let encoded_key = String::deserialize(deserializer)?; +// ClientMac::from_str(&encoded_key).map_err(serde::de::Error::custom) +// } +// } +// +// impl ClientPublicKey { +// #[allow(dead_code)] +// pub fn new(key: x25519_dalek::PublicKey) -> Self { +// ClientPublicKey(key) +// } +// +// pub fn as_bytes(&self) -> &[u8] { +// self.0.as_bytes() +// } +// } +// +// impl Deref for ClientPublicKey { +// type Target = x25519_dalek::PublicKey; +// +// fn deref(&self) -> &Self::Target { +// &self.0 +// } +// } +// +// impl FromStr for ClientPublicKey { +// type Err = String; +// +// fn from_str(s: &str) -> Result { +// let key_bytes: [u8; 32] = general_purpose::STANDARD +// .decode(s) +// .map_err(|_| "Could not base64 decode public key representation".to_string())? +// .try_into() +// .map_err(|_| "Invalid key length".to_string())?; +// Ok(ClientPublicKey(x25519_dalek::PublicKey::from(key_bytes))) +// } +// } +// +// impl Serialize for ClientPublicKey { +// fn serialize(&self, serializer: S) -> Result { +// let encoded_key = general_purpose::STANDARD.encode(self.0.as_bytes()); +// serializer.serialize_str(&encoded_key) +// } +// } +// +// impl<'de> Deserialize<'de> for ClientPublicKey { +// fn deserialize>(deserializer: D) -> Result { +// let encoded_key = String::deserialize(deserializer)?; +// Ok(ClientPublicKey::from_str(&encoded_key).map_err(serde::de::Error::custom))? +// } +// } +// +// pub(crate) type ClientRegistry = HashMap; diff --git a/gateway/src/node/http/api/mod.rs b/gateway/src/node/http/api/mod.rs deleted file mode 100644 index c0753a12f8..0000000000 --- a/gateway/src/node/http/api/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod v1; diff --git a/gateway/src/node/http/api/v1/client_registry.rs b/gateway/src/node/http/api/v1/client_registry.rs deleted file mode 100644 index 8e0a63b03a..0000000000 --- a/gateway/src/node/http/api/v1/client_registry.rs +++ /dev/null @@ -1,105 +0,0 @@ -use axum::extract::Path; -use std::sync::Arc; - -use axum::http::StatusCode; -use axum::{extract::State, Json}; -use std::str::FromStr; - -// use axum_macros::debug_handler; - -use crate::node::client_handling::client_registration::{ - Client, ClientMessage, ClientPublicKey, InitMessage, -}; -use crate::node::http::ApiState; - -async fn process_final_message(client: Client, state: Arc) -> StatusCode { - let preshared_nonce = { - let in_progress_ro = state.registration_in_progress.read().await; - if let Some(nonce) = in_progress_ro.get(client.pub_key()) { - *nonce - } else { - return StatusCode::BAD_REQUEST; - } - }; - - if client - .verify(state.sphinx_key_pair.private_key(), preshared_nonce) - .is_ok() - { - { - let mut in_progress_rw = state.registration_in_progress.write().await; - in_progress_rw.remove(client.pub_key()); - } - { - let mut registry_rw = state.client_registry.write().await; - registry_rw.insert(client.socket(), client); - } - return StatusCode::OK; - } - - StatusCode::BAD_REQUEST -} - -async fn process_init_message(init_message: InitMessage, state: Arc) -> u64 { - let nonce: u64 = fastrand::u64(..); - let mut registry_rw = state.registration_in_progress.write().await; - registry_rw.insert(init_message.pub_key().clone(), nonce); - nonce -} - -// #[debug_handler] -pub(crate) async fn register_client( - State(state): State>, - Json(payload): Json, -) -> (StatusCode, Json>) { - match payload { - ClientMessage::Init(i) => ( - StatusCode::OK, - Json(Some(process_init_message(i, Arc::clone(&state)).await)), - ), - ClientMessage::Final(client) => ( - process_final_message(client, Arc::clone(&state)).await, - Json(None), - ), - } -} - -pub(crate) async fn get_all_clients( - State(state): State>, -) -> (StatusCode, Json>) { - let registry_ro = state.client_registry.read().await; - ( - StatusCode::OK, - Json( - registry_ro - .values() - .map(|c| c.pub_key().clone()) - .collect::>(), - ), - ) -} - -pub(crate) async fn get_client( - Path(pub_key): Path, - State(state): State>, -) -> (StatusCode, Json>) { - let pub_key = match ClientPublicKey::from_str(&pub_key) { - Ok(pub_key) => pub_key, - Err(_) => return (StatusCode::BAD_REQUEST, Json(vec![])), - }; - let registry_ro = state.client_registry.read().await; - let clients = registry_ro - .iter() - .filter_map(|(_, c)| { - if c.pub_key() == &pub_key { - Some(c.clone()) - } else { - None - } - }) - .collect::>(); - if clients.is_empty() { - return (StatusCode::NOT_FOUND, Json(clients)); - } - (StatusCode::OK, Json(clients)) -} diff --git a/gateway/src/node/http/api/v1/mod.rs b/gateway/src/node/http/api/v1/mod.rs deleted file mode 100644 index 082c8dd954..0000000000 --- a/gateway/src/node/http/api/v1/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod client_registry; diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index f1b803973e..d7f853b299 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -1,7 +1,6 @@ // Copyright 2020-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use self::client_handling::client_registration::ClientRegistry; use self::storage::PersistentStorage; use crate::commands::helpers::{override_network_requester_config, OverrideNetworkRequesterConfig}; use crate::config::Config; @@ -14,7 +13,6 @@ use crate::node::client_handling::embedded_network_requester::{ use crate::node::client_handling::websocket; use crate::node::client_handling::websocket::connection_handler::coconut::CoconutVerifier; use crate::node::helpers::{initialise_main_storage, load_network_requester_config}; -use crate::node::http::start_http_api; use crate::node::mixnet_handling::receiver::connection_handler::ConnectionHandler; use crate::node::statistics::collector::GatewayStatisticsCollector; use crate::node::storage::Storage; @@ -25,6 +23,7 @@ use nym_crypto::asymmetric::{encryption, identity}; use nym_mixnet_client::forwarder::{MixForwardingSender, PacketForwarder}; use nym_network_defaults::NymNetworkDetails; use nym_network_requester::{LocalGateway, NRServiceProviderBuilder}; +use nym_node::wireguard::types::ClientRegistry; use nym_statistics_common::collector::StatisticsSender; use nym_task::{TaskClient, TaskManager}; use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; @@ -39,7 +38,6 @@ use tokio::sync::RwLock; pub(crate) mod client_handling; pub(crate) mod helpers; -pub(crate) mod http; pub(crate) mod mixnet_handling; pub(crate) mod statistics; pub(crate) mod storage; @@ -337,6 +335,9 @@ impl Gateway { start_http_api( &self.config, self.network_requester_opts.as_ref().map(|o| &o.config), + self.client_registry.clone(), + self.identity_keypair.as_ref(), + self.sphinx_keypair.clone(), shutdown.subscribe().named("http-api"), )?; @@ -391,12 +392,6 @@ impl Gateway { bail!("{err}") } - // This should likely be wireguard feature gated, but its easier to test if it hangs in here - tokio::spawn(start_http_api( - Arc::clone(&self.client_registry), - Arc::clone(&self.sphinx_keypair), - )); - info!("Finished nym gateway startup procedure - it should now be able to receive mix and client traffic!"); if let Err(err) = Self::wait_for_interrupt(shutdown).await { diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index c33045110c..b56db43905 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -19,7 +19,15 @@ serde_yaml = "0.9.25" serde_json = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } -tokio = { workspace = true, features = ["macros"] } +tokio = { workspace = true, features = ["macros", "sync"] } + +# Wireguard: +base64 = "0.21.4" +sha2 = "0.10.8" +hmac = "0.12.1" +fastrand = "2" +x25519-dalek = { version = "2.0.0", features = ["static_secrets"] } + # HTTP API: axum = { workspace = true } @@ -41,6 +49,11 @@ utoipa-swagger-ui = { workspace = true, features = ["axum"] } #utoipa-redoc = { version = "0.1.0", features = ["axum"] } #utoipa-rapidoc = { version = "0.1.0", features = ["axum"] } - +nym-crypto = { path = "../common/crypto", features = ["asymmetric" ]} nym-node-requests = { path = "nym-node-requests", default-features = false, features = ["openapi"]} nym-task = { path = "../common/task" } + + +[dev-dependencies] +rand = "0.7.3" +nym-crypto = { path = "../common/crypto", features = ["rand" ]} diff --git a/nym-node/nym-node-requests/src/api/v1/gateway/client_interefaces/mod.rs b/nym-node/nym-node-requests/src/api/v1/gateway/client_interefaces/mod.rs new file mode 100644 index 0000000000..c67bf7db3b --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/gateway/client_interefaces/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod wireguard; \ No newline at end of file diff --git a/nym-node/nym-node-requests/src/api/v1/gateway/client_interefaces/wireguard/mod.rs b/nym-node/nym-node-requests/src/api/v1/gateway/client_interefaces/wireguard/mod.rs new file mode 100644 index 0000000000..afafedca78 --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/gateway/client_interefaces/wireguard/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod models; \ No newline at end of file diff --git a/nym-node/nym-node-requests/src/api/v1/gateway/client_interefaces/wireguard/models.rs b/nym-node/nym-node-requests/src/api/v1/gateway/client_interefaces/wireguard/models.rs new file mode 100644 index 0000000000..65161deb14 --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/gateway/client_interefaces/wireguard/models.rs @@ -0,0 +1,36 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[serde(tag = "type", rename_all = "camelCase")] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub enum ClientMessage { + Initial(InitMessage), + Final(Client), +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct InitMessage { + /// Base64 encoded x25519 public key + pub pub_key: String, +} + +/// Client that wants to register sends its PublicKey and SocketAddr bytes mac digest encrypted with a DH shared secret. +/// Gateway/Nym node can then verify pub_key payload using the same process +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct Client { + /// Base64 encoded x25519 public key + pub pub_key: String, + + /// Client's socket address + #[cfg_attr(feature = "openapi", schema(example = "1.2.3.4:51820"))] + pub socket: String, + + /// Sha256 hmac on the data (alongside the prior nonce) + pub mac: String, +} diff --git a/nym-node/nym-node-requests/src/api/v1/gateway/mod.rs b/nym-node/nym-node-requests/src/api/v1/gateway/mod.rs index c57081432b..addfd207d3 100644 --- a/nym-node/nym-node-requests/src/api/v1/gateway/mod.rs +++ b/nym-node/nym-node-requests/src/api/v1/gateway/mod.rs @@ -2,3 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 pub mod models; +pub mod client_interefaces; \ No newline at end of file diff --git a/nym-node/nym-node-requests/src/lib.rs b/nym-node/nym-node-requests/src/lib.rs index 3bf2d61db6..660807d0ff 100644 --- a/nym-node/nym-node-requests/src/lib.rs +++ b/nym-node/nym-node-requests/src/lib.rs @@ -63,6 +63,16 @@ pub mod routes { client_interfaces_absolute(), WEBSOCKETS ); + + pub mod wireguard { + use super::*; + + pub const CLIENT: &str = "/client"; + pub const CLIENTS: &str = "/clients"; + + absolute_route!(client_absolute, wireguard_absolute(), CLIENT); + absolute_route!(clients_absolute, wireguard_absolute(), CLIENTS); + } } } diff --git a/nym-node/src/error.rs b/nym-node/src/error.rs index 61a364758c..206e9adf40 100644 --- a/nym-node/src/error.rs +++ b/nym-node/src/error.rs @@ -1,6 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::wireguard::error::WireguardError; use std::net::SocketAddr; use thiserror::Error; @@ -18,6 +19,12 @@ pub enum NymNodeError { source: nym_node_requests::error::Error, }, + #[error(transparent)] + WireguardError { + #[from] + source: WireguardError, + }, + #[error("unimplemented")] Unimplemented, } diff --git a/nym-node/src/http/middleware/logging.rs b/nym-node/src/http/middleware/logging.rs index 8ab0c41946..0253a11c1c 100644 --- a/nym-node/src/http/middleware/logging.rs +++ b/nym-node/src/http/middleware/logging.rs @@ -31,7 +31,7 @@ pub async fn logger(req: Request, next: Next) -> impl IntoResponse { status.to_string().yellow() }; - info!("[{host}] {method} '{uri}': {print_status} / agent: {agent}"); + info!(target: "incoming request", "[{host}] {method} '{uri}': {print_status} / agent: {agent}"); res } diff --git a/nym-node/src/http/router/api/mod.rs b/nym-node/src/http/router/api/mod.rs index 523f545bdc..95b7198619 100644 --- a/nym-node/src/http/router/api/mod.rs +++ b/nym-node/src/http/router/api/mod.rs @@ -1,7 +1,6 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::http::state::AppState; use axum::http::{header, HeaderValue, StatusCode}; use axum::response::{IntoResponse, Response}; use axum::{Json, Router}; @@ -11,6 +10,7 @@ use utoipa::{IntoParams, ToSchema}; pub mod v1; +use crate::http::api::v1::gateway::client_interfaces::wireguard::WireguardAppState; pub use nym_node_requests::api as api_requests; use nym_node_requests::routes; @@ -19,8 +19,14 @@ pub struct Config { pub v1_config: v1::Config, } -pub(super) fn routes(config: Config) -> Router { - Router::new().nest(routes::api::V1, v1::routes(config.v1_config)) +pub(super) fn routes( + config: Config, + initial_wg_state: WireguardAppState, +) -> Router { + Router::new().nest( + routes::api::V1, + v1::routes(config.v1_config, initial_wg_state), + ) } #[derive(Debug, Clone, ToSchema)] diff --git a/nym-node/src/http/router/api/v1/gateway/client_interfaces/mod.rs b/nym-node/src/http/router/api/v1/gateway/client_interfaces/mod.rs index 4582bff380..a9b4f6beb7 100644 --- a/nym-node/src/http/router/api/v1/gateway/client_interfaces/mod.rs +++ b/nym-node/src/http/router/api/v1/gateway/client_interfaces/mod.rs @@ -1,8 +1,8 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::http::api::v1::gateway::client_interfaces::wireguard::WireguardAppState; use crate::http::api::{FormattedResponse, OutputParams}; -use crate::http::state::AppState; use axum::extract::Query; use axum::http::StatusCode; use axum::routing::get; @@ -10,7 +10,12 @@ use axum::Router; use nym_node_requests::api::v1::gateway::models::{ClientInterfaces, WebSockets, Wireguard}; use nym_node_requests::routes::api::v1::gateway::client_interfaces; -pub(crate) fn routes(interfaces: Option) -> Router { +pub(crate) mod wireguard; + +pub(crate) fn routes( + interfaces: Option, + initial_wg_state: WireguardAppState, +) -> Router { Router::new() .route( "/", @@ -26,11 +31,15 @@ pub(crate) fn routes(interfaces: Option) -> Router { move |query| mixnet_websockets(websockets, query) }), ) + .nest( + client_interfaces::WIREGUARD, + wireguard::routes(initial_wg_state), + ) .route( client_interfaces::WIREGUARD, get({ - let wireguard_info = interfaces.and_then(|i| i.wireguard); - move |query| wireguard(wireguard_info, query) + let wireguard_cfg_info = interfaces.and_then(|i| i.wireguard); + move |query| wireguard_info(wireguard_cfg_info, query) }), ) } @@ -76,7 +85,7 @@ pub type ClientInterfacesResponse = FormattedResponse; ), params(OutputParams) )] -pub(crate) async fn wireguard( +pub(crate) async fn wireguard_info( wireguard: Option, Query(output): Query, ) -> Result { diff --git a/nym-node/src/http/router/api/v1/gateway/client_interfaces/wireguard/client_registry.rs b/nym-node/src/http/router/api/v1/gateway/client_interfaces/wireguard/client_registry.rs new file mode 100644 index 0000000000..93f18a04bc --- /dev/null +++ b/nym-node/src/http/router/api/v1/gateway/client_interfaces/wireguard/client_registry.rs @@ -0,0 +1,126 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::http::api::v1::gateway::client_interfaces::wireguard::{ + WireguardAppState, WireguardAppStateInner, +}; +use crate::wireguard::types::{ + Client, ClientMessage, ClientMessageRequest, ClientPublicKey, InitMessage, +}; +use axum::extract::{Path, State}; +use axum::http::StatusCode; +use axum::Json; +use std::str::FromStr; +use tracing::warn; + +async fn process_final_message(client: Client, state: &WireguardAppStateInner) -> StatusCode { + let preshared_nonce = { + let in_progress_ro = state.registration_in_progress.read().await; + if let Some(nonce) = in_progress_ro.get(&client.pub_key()) { + *nonce + } else { + return StatusCode::BAD_REQUEST; + } + }; + + if client + .verify(state.dh_keypair.private_key(), preshared_nonce) + .is_ok() + { + { + let mut in_progress_rw = state.registration_in_progress.write().await; + in_progress_rw.remove(&client.pub_key()); + } + { + let mut registry_rw = state.client_registry.write().await; + registry_rw.insert(client.socket(), client); + } + return StatusCode::OK; + } + + StatusCode::BAD_REQUEST +} + +async fn process_init_message(init_message: InitMessage, state: &WireguardAppStateInner) -> u64 { + let nonce: u64 = fastrand::u64(..); + let mut registry_rw = state.registration_in_progress.write().await; + registry_rw.insert(*init_message.pub_key(), nonce); + nonce +} + +pub(crate) async fn register_client( + State(state): State, + Json(payload): Json, +) -> (StatusCode, Json>) { + let Some(state) = state.inner() else { + return (StatusCode::NOT_IMPLEMENTED, Json(None)); + }; + + match ClientMessage::try_from(payload) { + Ok(payload) => match payload { + ClientMessage::Init(i) => ( + StatusCode::OK, + Json(Some(process_init_message(i, state).await)), + ), + ClientMessage::Final(client) => { + (process_final_message(client, state).await, Json(None)) + } + }, + Err(err) => { + warn!("failed to deserialize received request: {err}"); + (StatusCode::BAD_REQUEST, Json(None)) + } + } +} + +// pub type RegisterClientResponse = FormattedResponse<()>; +pub(crate) async fn get_all_clients( + State(state): State, +) -> (StatusCode, Json>) { + let Some(state) = state.inner() else { + return (StatusCode::NOT_IMPLEMENTED, Json(Vec::new())); + }; + let registry_ro = state.client_registry.read().await; + ( + StatusCode::OK, + Json( + registry_ro + .values() + .map(|c| c.pub_key()) + .collect::>(), + ), + ) +} + +// pub type AllClientsResponse = FormattedResponse<()>; + +pub(crate) async fn get_client( + Path(pub_key): Path, + State(state): State, +) -> (StatusCode, Json>) { + let Some(state) = state.inner() else { + return (StatusCode::NOT_IMPLEMENTED, Json(Vec::new())); + }; + let pub_key = match ClientPublicKey::from_str(&pub_key) { + Ok(pub_key) => pub_key, + Err(_) => return (StatusCode::BAD_REQUEST, Json(vec![])), + }; + + let registry_ro = state.client_registry.read().await; + let clients = registry_ro + .iter() + .filter_map(|(_, c)| { + if c.pub_key() == pub_key { + Some(c.clone()) + } else { + None + } + }) + .collect::>(); + if clients.is_empty() { + return (StatusCode::NOT_FOUND, Json(clients)); + } + (StatusCode::OK, Json(clients)) +} + +// pub type ClientResponse = FormattedResponse<()>; diff --git a/gateway/src/node/http/mod.rs b/nym-node/src/http/router/api/v1/gateway/client_interfaces/wireguard/mod.rs similarity index 52% rename from gateway/src/node/http/mod.rs rename to nym-node/src/http/router/api/v1/gateway/client_interfaces/wireguard/mod.rs index 9d091f232f..4db60e5eb5 100644 --- a/gateway/src/node/http/mod.rs +++ b/nym-node/src/http/router/api/v1/gateway/client_interfaces/wireguard/mod.rs @@ -1,86 +1,112 @@ -use std::{collections::HashMap, sync::Arc}; +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 -use axum::{ - routing::{get, post}, - Router, +use crate::http::api::v1::gateway::client_interfaces::wireguard::client_registry::{ + get_all_clients, get_client, register_client, }; -use log::info; +use crate::wireguard::types::{ClientRegistry, PendingRegistrations}; +use axum::routing::{get, post}; +use axum::Router; use nym_crypto::asymmetric::encryption; +use nym_node_requests::routes::api::v1::gateway::client_interfaces::wireguard; +use std::sync::Arc; use tokio::sync::RwLock; -mod api; -use api::v1::client_registry::*; +pub(crate) mod client_registry; -use super::client_handling::client_registration::{ClientPublicKey, ClientRegistry}; - -const ROUTE_PREFIX: &str = "/api/v1/gateway/client-interfaces/wireguard"; - -pub struct ApiState { - client_registry: Arc>, - sphinx_key_pair: Arc, - registration_in_progress: Arc>>, +// I don't see any reason why this state should be accessible to any routes outside /wireguard +// if anyone finds compelling reason, it could be moved to the `AppState` struct instead +#[derive(Clone, Default)] +pub struct WireguardAppState { + inner: Option, } -fn router_with_state(state: Arc) -> Router { +impl WireguardAppState { + pub fn new( + dh_keypair: Arc, + client_registry: Arc>, + registration_in_progress: Arc>, + ) -> Self { + WireguardAppState { + inner: Some(WireguardAppStateInner { + dh_keypair, + client_registry, + registration_in_progress, + }), + } + } + + #[allow(dead_code)] + pub(crate) fn dh_keypair(&self) -> Option<&encryption::KeyPair> { + self.inner.as_ref().map(|s| s.dh_keypair.as_ref()) + } + + #[allow(dead_code)] + pub(crate) fn client_registry(&self) -> Option<&RwLock> { + self.inner.as_ref().map(|s| s.client_registry.as_ref()) + } + + #[allow(dead_code)] + pub(crate) fn registration_in_progress(&self) -> Option<&RwLock> { + self.inner + .as_ref() + .map(|s| s.registration_in_progress.as_ref()) + } + + // not sure what to feel about exposing this method + pub(crate) fn inner(&self) -> Option<&WireguardAppStateInner> { + self.inner.as_ref() + } +} + +// helper macro to deal with missing wg state (if not being exposed by the node) +#[macro_export] +macro_rules! get_state { + ( $state: ident, $field: ident ) => {{ + let Some(ref inner) = $state.inner else { + return ::axum::http::StatusCode::NOT_IMPLEMENTED; + }; + inner.$field.as_ref() + }}; +} + +#[derive(Clone)] +pub(crate) struct WireguardAppStateInner { + dh_keypair: Arc, + client_registry: Arc>, + registration_in_progress: Arc>, +} + +pub(crate) fn routes(initial_state: WireguardAppState) -> Router { Router::new() - .route(&format!("{}/clients", ROUTE_PREFIX), get(get_all_clients)) - .route(&format!("{}/client", ROUTE_PREFIX), post(register_client)) - .route( - &format!("{}/client/:pub_key", ROUTE_PREFIX), - get(get_client), - ) - .with_state(state) -} - -pub(crate) async fn start_http_api( - client_registry: Arc>, - sphinx_key_pair: Arc, -) { - // Port should be 80 post smoosh - let port = 8000; - - info!("Started HTTP API on port {}", port); - - let client_registry = Arc::clone(&client_registry); - - let state = Arc::new(ApiState { - client_registry, - sphinx_key_pair, - registration_in_progress: Arc::new(RwLock::new(HashMap::new())), - }); - - let routes = router_with_state(state); - - #[allow(clippy::unwrap_used)] - axum::Server::bind(&format!("0.0.0.0:{}", port).parse().unwrap()) - .serve(routes.into_make_service()) - .await - .unwrap(); + // .route("/", get()) + .route(wireguard::CLIENTS, get(get_all_clients)) + .route(wireguard::CLIENT, post(register_client)) + .route(&format!("{}/:pub_key", wireguard::CLIENT), get(get_client)) + .with_state(initial_state) } #[cfg(test)] mod test { - use std::net::SocketAddr; - use std::str::FromStr; - use std::{collections::HashMap, sync::Arc}; - + use crate::http::api::v1::gateway::client_interfaces::wireguard::{ + routes, WireguardAppState, WireguardAppStateInner, + }; + use crate::wireguard::types::{ + ClientMac, ClientMessageRequest, ClientPublicKey, ClientRequest, HmacSha256, + InitMessageRequest, + }; use axum::body::Body; use axum::http::Request; use axum::http::StatusCode; use hmac::Mac; + use nym_crypto::asymmetric::encryption; + use nym_node_requests::routes::api::v1::gateway::client_interfaces::wireguard; + use std::{collections::HashMap, sync::Arc}; + use tokio::sync::RwLock; use tower::Service; use tower::ServiceExt; - - use nym_crypto::asymmetric::encryption; - use tokio::sync::RwLock; use x25519_dalek::{PublicKey, StaticSecret}; - use crate::node::client_handling::client_registration::{ - Client, ClientMac, ClientMessage, InitMessage, - }; - use crate::node::client_handling::client_registration::{ClientPublicKey, HmacSha256}; - use crate::node::http::{router_with_state, ApiState, ROUTE_PREFIX}; - #[tokio::test] async fn registration() { // 1. Provision random keys for gateway and client @@ -108,22 +134,25 @@ mod test { let registration_in_progress = Arc::new(RwLock::new(HashMap::new())); let client_registry = Arc::new(RwLock::new(HashMap::new())); - let state = Arc::new(ApiState { - client_registry: Arc::clone(&client_registry), - sphinx_key_pair: Arc::new(gateway_key_pair), - registration_in_progress: Arc::clone(®istration_in_progress), - }); + let state = WireguardAppState { + inner: Some(WireguardAppStateInner { + client_registry: Arc::clone(&client_registry), + dh_keypair: Arc::new(gateway_key_pair), + registration_in_progress: Arc::clone(®istration_in_progress), + }), + }; // `Router` implements `tower::Service>` so we can // call it like any tower service, no need to run an HTTP server. - let mut app = router_with_state(state); + let mut app = routes(state); - let init_message = - ClientMessage::Init(InitMessage::new(ClientPublicKey::new(client_static_public))); + let init_message = ClientMessageRequest::Initial(InitMessageRequest { + pub_key: ClientPublicKey::new(client_static_public).to_string(), + }); let init_request = Request::builder() .method("POST") - .uri(format!("{}/client", ROUTE_PREFIX)) + .uri(wireguard::CLIENT) .header("Content-type", "application/json") .body(Body::from(serde_json::to_vec(&init_message).unwrap())) .unwrap(); @@ -150,15 +179,15 @@ mod test { mac.update(&nonce.unwrap().to_le_bytes()); let mac = mac.finalize().into_bytes(); - let finalized_message = ClientMessage::Final(Client { - pub_key: ClientPublicKey::new(client_static_public), - socket: SocketAddr::from_str("127.0.0.1:8080").unwrap(), - mac: ClientMac::new(mac.as_slice().to_vec()), + let finalized_message = ClientMessageRequest::Final(ClientRequest { + pub_key: ClientPublicKey::new(client_static_public).to_string(), + socket: "127.0.0.1:8080".to_string(), + mac: ClientMac::new(mac.as_slice().to_vec()).to_string(), }); let final_request = Request::builder() .method("POST") - .uri(format!("{}/client", ROUTE_PREFIX)) + .uri(wireguard::CLIENT) .header("Content-type", "application/json") .body(Body::from(serde_json::to_vec(&finalized_message).unwrap())) .unwrap(); @@ -175,7 +204,7 @@ mod test { let clients_request = Request::builder() .method("GET") - .uri(format!("{}/clients", ROUTE_PREFIX)) + .uri(wireguard::CLIENTS) .body(Body::empty()) .unwrap(); diff --git a/nym-node/src/http/router/api/v1/gateway/mod.rs b/nym-node/src/http/router/api/v1/gateway/mod.rs index 0f47799bdd..d2bba50118 100644 --- a/nym-node/src/http/router/api/v1/gateway/mod.rs +++ b/nym-node/src/http/router/api/v1/gateway/mod.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::http::state::AppState; +use crate::http::api::v1::gateway::client_interfaces::wireguard::WireguardAppState; use axum::routing::get; use axum::Router; use nym_node_requests::api::v1::gateway::models; @@ -15,7 +15,10 @@ pub struct Config { pub details: Option, } -pub(crate) fn routes(config: Config) -> Router { +pub(crate) fn routes( + config: Config, + initial_wg_state: WireguardAppState, +) -> Router { Router::new() .route( "/", @@ -26,6 +29,9 @@ pub(crate) fn routes(config: Config) -> Router { ) .nest( gateway::CLIENT_INTERFACES, - client_interfaces::routes(config.details.map(|g| g.client_interfaces)), + client_interfaces::routes( + config.details.map(|g| g.client_interfaces), + initial_wg_state, + ), ) } diff --git a/nym-node/src/http/router/api/v1/mixnode/mod.rs b/nym-node/src/http/router/api/v1/mixnode/mod.rs index c638745d3d..998a0edd3e 100644 --- a/nym-node/src/http/router/api/v1/mixnode/mod.rs +++ b/nym-node/src/http/router/api/v1/mixnode/mod.rs @@ -1,7 +1,6 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::http::state::AppState; use axum::routing::get; use axum::Router; use nym_node_requests::api::v1::mixnode::models; @@ -13,7 +12,7 @@ pub struct Config { pub details: Option, } -pub(crate) fn routes(config: Config) -> Router { +pub(crate) fn routes(config: Config) -> Router { Router::new().route( "/", get({ diff --git a/nym-node/src/http/router/api/v1/mod.rs b/nym-node/src/http/router/api/v1/mod.rs index 64b94c9344..ec72936079 100644 --- a/nym-node/src/http/router/api/v1/mod.rs +++ b/nym-node/src/http/router/api/v1/mod.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::http::state::AppState; +use crate::http::api::v1::gateway::client_interfaces::wireguard::WireguardAppState; use axum::Router; use nym_node_requests::routes::api::v1; @@ -19,9 +19,15 @@ pub struct Config { pub network_requester: network_requester::Config, } -pub(super) fn routes(config: Config) -> Router { +pub(super) fn routes( + config: Config, + initial_wg_state: WireguardAppState, +) -> Router { Router::new() - .nest(v1::GATEWAY, gateway::routes(config.gateway)) + .nest( + v1::GATEWAY, + gateway::routes(config.gateway, initial_wg_state), + ) .nest(v1::MIXNODE, mixnode::routes(config.mixnode)) .nest( v1::NETWORK_REQUESTER, diff --git a/nym-node/src/http/router/api/v1/network_requester/mod.rs b/nym-node/src/http/router/api/v1/network_requester/mod.rs index 728e42b600..aced86bad3 100644 --- a/nym-node/src/http/router/api/v1/network_requester/mod.rs +++ b/nym-node/src/http/router/api/v1/network_requester/mod.rs @@ -1,7 +1,6 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::http::state::AppState; use axum::routing::get; use axum::Router; use nym_node_requests::api::v1::network_requester::models; @@ -13,7 +12,7 @@ pub struct Config { pub details: Option, } -pub(crate) fn routes(config: Config) -> Router { +pub(crate) fn routes(config: Config) -> Router { Router::new().route( "/", get({ diff --git a/nym-node/src/http/router/api/v1/node/mod.rs b/nym-node/src/http/router/api/v1/node/mod.rs index 24d898e619..6644e75572 100644 --- a/nym-node/src/http/router/api/v1/node/mod.rs +++ b/nym-node/src/http/router/api/v1/node/mod.rs @@ -4,7 +4,6 @@ use crate::http::api::v1::node::build_information::build_information; use crate::http::api::v1::node::host_information::host_information; use crate::http::api::v1::node::roles::roles; -use crate::http::state::AppState; use axum::routing::get; use axum::Router; use nym_node_requests::api::v1::node::models; @@ -21,7 +20,7 @@ pub struct Config { pub roles: models::NodeRoles, } -pub(super) fn routes(config: Config) -> Router { +pub(super) fn routes(config: Config) -> Router { Router::new() .route( v1::BUILD_INFO, diff --git a/nym-node/src/http/router/api/v1/openapi.rs b/nym-node/src/http/router/api/v1/openapi.rs index ec0f85cf12..24498d8527 100644 --- a/nym-node/src/http/router/api/v1/openapi.rs +++ b/nym-node/src/http/router/api/v1/openapi.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 use crate::http::router::api; -use crate::http::state::AppState; use axum::Router; use nym_node_requests::api as api_requests; use nym_node_requests::routes::api::v1; @@ -18,7 +17,7 @@ use utoipa_swagger_ui::SwaggerUi; api::v1::node::roles::roles, api::v1::gateway::root::root_gateway, api::v1::gateway::client_interfaces::client_interfaces, - api::v1::gateway::client_interfaces::wireguard, + api::v1::gateway::client_interfaces::wireguard_info, api::v1::gateway::client_interfaces::mixnet_websockets, api::v1::mixnode::root::root_mixnode, api::v1::network_requester::root::root_network_requester, @@ -41,7 +40,7 @@ use utoipa_swagger_ui::SwaggerUi; )] pub(crate) struct ApiDoc; -pub(crate) fn route() -> Router { +pub(crate) fn route() -> Router { // provide absolute path to the openapi.json let config = utoipa_swagger_ui::Config::from("/api/v1/api-docs/openapi.json"); SwaggerUi::new(v1::SWAGGER) diff --git a/nym-node/src/http/router/landing_page.rs b/nym-node/src/http/router/landing_page.rs index 75f5f7f6af..84f64f798d 100644 --- a/nym-node/src/http/router/landing_page.rs +++ b/nym-node/src/http/router/landing_page.rs @@ -1,7 +1,6 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::http::state::AppState; use axum::routing::get; use axum::Router; use std::path::PathBuf; @@ -12,7 +11,7 @@ pub struct Config { pub assets_path: Option, } -pub(super) fn routes(config: Config) -> Router { +pub(super) fn routes(config: Config) -> Router { if let Some(assets) = config.assets_path { Router::new().nest_service("/", ServeDir::new(assets)) } else { diff --git a/nym-node/src/http/router/mod.rs b/nym-node/src/http/router/mod.rs index 81625845e9..5985b6f159 100644 --- a/nym-node/src/http/router/mod.rs +++ b/nym-node/src/http/router/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::NymNodeError; +pub use crate::http::api::v1::gateway::client_interfaces::wireguard::WireguardAppState; use crate::http::middleware::logging; use crate::http::state::AppState; use crate::http::NymNodeHTTPServer; @@ -72,13 +73,17 @@ pub struct NymNodeRouter { } impl NymNodeRouter { - pub fn new(config: Config) -> NymNodeRouter { + // TODO: move the wg state to a builder + pub fn new(config: Config, initial_wg_state: Option) -> NymNodeRouter { let state = AppState::new(); NymNodeRouter { inner: Router::new() .nest(routes::LANDING_PAGE, landing_page::routes(config.landing)) - .nest(routes::API, api::routes(config.api)) + .nest( + routes::API, + api::routes(config.api, initial_wg_state.unwrap_or_default()), + ) .layer(axum::middleware::from_fn(logging::logger)) .with_state(state), } diff --git a/nym-node/src/http/state.rs b/nym-node/src/http/state.rs index 7308157cb6..f7c8bfcc33 100644 --- a/nym-node/src/http/state.rs +++ b/nym-node/src/http/state.rs @@ -1,32 +1,29 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::ops::Deref; -use std::sync::Arc; - #[derive(Debug, Clone)] pub(crate) struct AppState { - inner: Arc, + // wireguard: WireguardAppState, } -// TODO: https://docs.rs/axum/latest/axum/extract/struct.State.html#substates +// #[derive(Debug, Clone)] +// pub struct WireguardAppState { +// // inner: Option, +// } +// +// #[derive(Debug)] +// pub(crate) struct WireguardAppStateInner { +// // +// } +// +// impl FromRef for WireguardAppState { +// fn from_ref(app_state: &AppState) -> Self { +// app_state.wireguard.clone() +// } +// } impl AppState { pub fn new() -> Self { - AppState { - inner: Arc::new(AppStateInner {}), - } + AppState {} } } - -// TODO: that's kinda abusing it... -impl Deref for AppState { - type Target = AppStateInner; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -#[derive(Debug)] -pub(crate) struct AppStateInner {} diff --git a/nym-node/src/lib.rs b/nym-node/src/lib.rs index f3ea3fafdf..ec9e691834 100644 --- a/nym-node/src/lib.rs +++ b/nym-node/src/lib.rs @@ -8,3 +8,4 @@ pub mod error; pub mod http; +pub mod wireguard; diff --git a/nym-node/src/wireguard/error.rs b/nym-node/src/wireguard/error.rs new file mode 100644 index 0000000000..292ef012af --- /dev/null +++ b/nym-node/src/wireguard/error.rs @@ -0,0 +1,43 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use hmac::digest::MacError; +use std::net::AddrParseError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum WireguardError { + #[error("the provided base64-encoded client MAC ('{mac}') was malformed: {source}")] + MalformedClientMac { + mac: String, + #[source] + source: base64::DecodeError, + }, + + #[error("the provided base64-encoded client x25519 public key ('{pub_key}') was malformed: {source}")] + MalformedClientPublicKeyEncoding { + pub_key: String, + #[source] + source: base64::DecodeError, + }, + + #[error("the provided base64-encoded client x25519 public key ('{pub_key}') has invalid length: {decoded_length}. expected 32 bytes")] + InvalidClientPublicKeyLength { + pub_key: String, + decoded_length: usize, + }, + + #[error("failed to verify mac provided by '{client}': {source}")] + FailedClientMacVerification { + client: String, + #[source] + source: MacError, + }, + + #[error("the provided client socket address ('{raw}') was malformed: {source}")] + MalformedClientSocketAddress { + raw: String, + #[source] + source: AddrParseError, + }, +} diff --git a/nym-node/src/wireguard/mod.rs b/nym-node/src/wireguard/mod.rs new file mode 100644 index 0000000000..ad3a0d85cd --- /dev/null +++ b/nym-node/src/wireguard/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// obviously most of the features currently reside in the gateway, +// but let's start putting everything in here + +pub mod error; +pub mod types; diff --git a/nym-node/src/wireguard/types.rs b/nym-node/src/wireguard/types.rs new file mode 100644 index 0000000000..b3bb755278 --- /dev/null +++ b/nym-node/src/wireguard/types.rs @@ -0,0 +1,258 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::wireguard::error::WireguardError; +use base64::{engine::general_purpose, Engine}; +use hmac::{Hmac, Mac}; +use nym_crypto::asymmetric::encryption::PrivateKey; +pub(crate) use nym_node_requests::api::v1::gateway::client_interefaces::wireguard::models::{ + Client as ClientRequest, ClientMessage as ClientMessageRequest, + InitMessage as InitMessageRequest, +}; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use std::collections::HashMap; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::net::SocketAddr; +use std::ops::Deref; +use std::str::FromStr; +use x25519_dalek::StaticSecret; + +pub type HmacSha256 = Hmac; +pub type ClientRegistry = HashMap; +pub type Nonce = u64; +pub type PendingRegistrations = HashMap; + +#[derive(Debug, Clone)] +pub(crate) enum ClientMessage { + Init(InitMessage), + Final(Client), +} + +impl TryFrom for ClientMessage { + type Error = WireguardError; + + fn try_from(value: ClientMessageRequest) -> Result { + match value { + ClientMessageRequest::Initial(init) => init.try_into().map(ClientMessage::Init), + ClientMessageRequest::Final(final_msg) => { + final_msg.try_into().map(ClientMessage::Final) + } + } + } +} + +#[derive(Debug, Clone)] +pub(crate) struct InitMessage { + pub pub_key: ClientPublicKey, +} + +impl TryFrom for InitMessage { + type Error = WireguardError; + + fn try_from(value: InitMessageRequest) -> Result { + Ok(InitMessage { + pub_key: value.pub_key.parse()?, + }) + } +} + +impl InitMessage { + pub fn pub_key(&self) -> &ClientPublicKey { + &self.pub_key + } + + #[allow(dead_code)] + pub fn new(pub_key: ClientPublicKey) -> Self { + InitMessage { pub_key } + } +} + +// Client that wants to register sends its PublicKey and SocketAddr bytes mac digest encrypted with a DH shared secret. +// Gateway can then verify pub_key payload using the sme process +#[derive(Debug, Clone)] +// TEMP: +#[derive(Serialize)] +pub struct Client { + // base64 encoded public key, using x25519-dalek for impl + pub pub_key: ClientPublicKey, + pub socket: SocketAddr, + pub mac: ClientMac, +} + +impl TryFrom for Client { + type Error = WireguardError; + + fn try_from(value: ClientRequest) -> Result { + Ok(Client { + pub_key: value.pub_key.parse()?, + socket: value.socket.parse().map_err(|source| { + WireguardError::MalformedClientSocketAddress { + raw: value.socket, + source, + } + })?, + mac: value.mac.parse()?, + }) + } +} + +impl Client { + // Reusable secret should be gateways Wireguard PK + // Client should perform this step when generating its payload, using its own WG PK + pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), WireguardError> { + // convert from 1.0 x25519-dalek private key into 2.0 x25519-dalek + #[allow(clippy::expect_used)] + let static_secret = StaticSecret::try_from(gateway_key.to_bytes()) + .expect("conversion between x25519 private keys is infallible"); + + let dh = static_secret.diffie_hellman(&self.pub_key); + + // TODO: change that to use our nym_crypto::hmac module instead + + #[allow(clippy::expect_used)] + let mut mac = HmacSha256::new_from_slice(dh.as_bytes()) + .expect("x25519 shared secret is always 32 bytes long"); + + mac.update(self.pub_key.as_bytes()); + mac.update(self.socket.ip().to_string().as_bytes()); + mac.update(self.socket.port().to_string().as_bytes()); + mac.update(&nonce.to_le_bytes()); + + mac.verify_slice(&self.mac) + .map_err(|source| WireguardError::FailedClientMacVerification { + client: self.pub_key.to_string(), + source, + }) + } + + pub fn pub_key(&self) -> ClientPublicKey { + self.pub_key + } + + pub fn socket(&self) -> SocketAddr { + self.socket + } +} + +// This should go into nym-wireguard crate +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct ClientPublicKey(x25519_dalek::PublicKey); + +// TODO: change the inner type into generic array of size HmacSha256::OutputSize +#[derive(Debug, Clone)] +pub struct ClientMac(Vec); + +impl fmt::Display for ClientMac { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", general_purpose::STANDARD.encode(&self.0)) + } +} + +impl ClientMac { + #[allow(dead_code)] + pub fn new(mac: Vec) -> Self { + ClientMac(mac) + } +} + +impl Deref for ClientMac { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl fmt::Display for ClientPublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", general_purpose::STANDARD.encode(self.0.as_bytes())) + } +} + +impl Hash for ClientPublicKey { + fn hash(&self, state: &mut H) { + self.0.as_bytes().hash(state) + } +} + +impl FromStr for ClientMac { + type Err = WireguardError; + + fn from_str(s: &str) -> Result { + let mac_bytes: Vec = general_purpose::STANDARD.decode(s).map_err(|source| { + WireguardError::MalformedClientMac { + mac: s.to_string(), + source, + } + })?; + + Ok(ClientMac(mac_bytes)) + } +} + +impl ClientPublicKey { + #[allow(dead_code)] + pub fn new(key: x25519_dalek::PublicKey) -> Self { + ClientPublicKey(key) + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +impl Deref for ClientPublicKey { + type Target = x25519_dalek::PublicKey; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromStr for ClientPublicKey { + type Err = WireguardError; + + fn from_str(s: &str) -> Result { + let key_bytes: Vec = general_purpose::STANDARD.decode(s).map_err(|source| { + WireguardError::MalformedClientPublicKeyEncoding { + pub_key: s.to_string(), + source, + } + })?; + + let decoded_length = key_bytes.len(); + let Ok(key_arr): Result<[u8; 32], _> = key_bytes.try_into() else { + return Err(WireguardError::InvalidClientPublicKeyLength { + pub_key: s.to_string(), + decoded_length, + })?; + }; + + Ok(ClientPublicKey(x25519_dalek::PublicKey::from(key_arr))) + } +} + +// TEMPORARY: + +impl Serialize for ClientPublicKey { + fn serialize(&self, serializer: S) -> Result { + let encoded_key = general_purpose::STANDARD.encode(self.0.as_bytes()); + serializer.serialize_str(&encoded_key) + } +} + +impl<'de> Deserialize<'de> for ClientPublicKey { + fn deserialize>(deserializer: D) -> Result { + let encoded_key = String::deserialize(deserializer)?; + Ok(ClientPublicKey::from_str(&encoded_key).map_err(serde::de::Error::custom))? + } +} + +impl Serialize for ClientMac { + fn serialize(&self, serializer: S) -> Result { + let encoded_key = general_purpose::STANDARD.encode(&self.0); + serializer.serialize_str(&encoded_key) + } +}