From 1e900c32dfaab9ce5dc614559cfead87d6c6f4ed Mon Sep 17 00:00:00 2001 From: Drazen Urch Date: Tue, 10 Oct 2023 10:55:26 +0200 Subject: [PATCH] Gateway client registry and api routes (#3955) * Add HTTP API and Client registry to Gateway * Update CHANGELOG * Smooshify structure * Reify x25519 public key * Hmac message verification * Add lightweight handshake with replay protection * Tidy up, move registartion to its own file * Test for the registration flow * Fix nonce loop hole --- CHANGELOG.md | 3 + Cargo.lock | 92 +++++--- gateway/Cargo.toml | 27 ++- gateway/src/error.rs | 10 + .../client_handling/client_registration.rs | 181 +++++++++++++++ gateway/src/node/client_handling/mod.rs | 1 + .../websocket/connection_handler/coconut.rs | 2 +- gateway/src/node/http/api/mod.rs | 1 + .../src/node/http/api/v1/client_registry.rs | 105 +++++++++ gateway/src/node/http/api/v1/mod.rs | 1 + gateway/src/node/http/mod.rs | 206 ++++++++++++++++++ gateway/src/node/mod.rs | 15 ++ .../network-requester/src/core.rs | 2 +- 13 files changed, 614 insertions(+), 32 deletions(-) create mode 100644 gateway/src/node/client_handling/client_registration.rs create mode 100644 gateway/src/node/http/api/mod.rs create mode 100644 gateway/src/node/http/api/v1/client_registry.rs create mode 100644 gateway/src/node/http/api/v1/mod.rs create mode 100644 gateway/src/node/http/mod.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index fe08ef04e3..0db3dfaf1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- add client registry to Gateway ([#3955]) +- add HTTP API to Gateway ([#3955]) +- add `/client/`, `clients` and `register` routes to the gateway ([#3955]) ## [2023.1-milka] (2023-09-24) diff --git a/Cargo.lock b/Cargo.lock index d1f7a1a1e8..c399b9b877 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -766,7 +766,11 @@ dependencies = [ "pin-project-lite 0.2.12", "rustversion", "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", "sync_wrapper", + "tokio", "tower", "tower-layer", "tower-service", @@ -789,6 +793,18 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-macros" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdca6a10ecad987bda04e95606ef85a5417dcaac1a78455242d72e031e2b6b62" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "backtrace" version = "0.3.68" @@ -868,7 +884,7 @@ dependencies = [ "pbkdf2", "rand_core 0.6.4", "ripemd", - "sha2 0.10.7", + "sha2 0.10.8", "subtle 2.4.1", "zeroize", ] @@ -1076,7 +1092,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5353f36341f7451062466f0b755b96ac3a9547e4d7f6b70d603fc721a7d7896" dependencies = [ - "sha2 0.10.7", + "sha2 0.10.8", ] [[package]] @@ -1739,7 +1755,7 @@ dependencies = [ "schemars", "serde", "serde-json-wasm", - "sha2 0.10.7", + "sha2 0.10.8", "thiserror", ] @@ -4365,7 +4381,7 @@ dependencies = [ "cfg-if", "ecdsa 0.14.8", "elliptic-curve 0.12.3", - "sha2 0.10.7", + "sha2 0.10.8", ] [[package]] @@ -4378,7 +4394,7 @@ dependencies = [ "ecdsa 0.16.8", "elliptic-curve 0.13.5", "once_cell", - "sha2 0.10.7", + "sha2 0.10.8", "signature 2.1.0", ] @@ -4612,7 +4628,7 @@ dependencies = [ "rand 0.8.5", "rw-stream-sink 0.3.0 (git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6)", "sec1 0.3.0", - "sha2 0.10.7", + "sha2 0.10.8", "smallvec", "thiserror", "unsigned-varint", @@ -4698,7 +4714,7 @@ dependencies = [ "prost-codec", "rand 0.8.5", "regex", - "sha2 0.10.7", + "sha2 0.10.8", "smallvec", "thiserror", "unsigned-varint", @@ -4730,7 +4746,7 @@ dependencies = [ "rand 0.8.5", "regex", "serde", - "sha2 0.10.7", + "sha2 0.10.8", "smallvec", "thiserror", "unsigned-varint", @@ -4773,7 +4789,7 @@ dependencies = [ "quick-protobuf", "rand 0.8.5", "serde", - "sha2 0.10.7", + "sha2 0.10.8", "thiserror", "zeroize", ] @@ -4799,7 +4815,7 @@ dependencies = [ "quick-protobuf", "rand 0.8.5", "serde", - "sha2 0.10.7", + "sha2 0.10.8", "smallvec", "thiserror", "uint", @@ -4904,7 +4920,7 @@ dependencies = [ "prost", "prost-build", "rand 0.8.5", - "sha2 0.10.7", + "sha2 0.10.8", "snow", "static_assertions", "thiserror", @@ -4927,7 +4943,7 @@ dependencies = [ "once_cell", "quick-protobuf", "rand 0.8.5", - "sha2 0.10.7", + "sha2 0.10.8", "snow", "static_assertions", "thiserror", @@ -5616,7 +5632,7 @@ dependencies = [ "multihash-derive", "serde", "serde-big-array", - "sha2 0.10.7", + "sha2 0.10.8", "unsigned-varint", ] @@ -6193,7 +6209,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sha2 0.10.7", + "sha2 0.10.8", "sqlx 0.6.3", "tap", "tempfile", @@ -6463,6 +6479,9 @@ dependencies = [ "anyhow", "async-trait", "atty", + "axum", + "axum-macros", + "base64 0.21.4", "bip39", "bs58 0.4.0", "clap 4.3.21", @@ -6470,8 +6489,11 @@ dependencies = [ "dashmap", "dirs 4.0.0", "dotenvy", + "fastrand 2.0.0", "futures", + "hmac 0.12.1", "humantime-serde", + "hyper", "lazy_static", "log", "nym-api-requests", @@ -6495,8 +6517,10 @@ dependencies = [ "once_cell", "pretty_env_logger", "rand 0.7.3", + "rand 0.8.5", "serde", "serde_json", + "sha2 0.10.8", "sqlx 0.5.11", "subtle-encoding", "thiserror", @@ -6504,7 +6528,9 @@ dependencies = [ "tokio-stream", "tokio-tungstenite", "tokio-util", + "tower", "url", + "x25519-dalek 2.0.0", "zeroize", ] @@ -7694,7 +7720,7 @@ checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" dependencies = [ "ecdsa 0.14.8", "elliptic-curve 0.12.3", - "sha2 0.10.7", + "sha2 0.10.8", ] [[package]] @@ -7705,7 +7731,7 @@ checksum = "dfc8c5bf642dde52bb9e87c0ecd8ca5a76faac2eeed98dedb7c717997e1080aa" dependencies = [ "ecdsa 0.14.8", "elliptic-curve 0.12.3", - "sha2 0.10.7", + "sha2 0.10.8", ] [[package]] @@ -7939,7 +7965,7 @@ checksum = "56af0a30af74d0445c0bf6d9d051c979b516a1a5af790d251daee76005420a48" dependencies = [ "once_cell", "pest", - "sha2 0.10.7", + "sha2 0.10.8", ] [[package]] @@ -9075,7 +9101,7 @@ version = "7.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512b0ab6853f7e14e3c8754acb43d6f748bb9ced66aa5915a6553ac8213f7731" dependencies = [ - "sha2 0.10.7", + "sha2 0.10.8", "walkdir", ] @@ -9569,6 +9595,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.16" @@ -9640,9 +9676,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -9796,7 +9832,7 @@ dependencies = [ "rand_core 0.6.4", "ring", "rustc_version 0.4.0", - "sha2 0.10.7", + "sha2 0.10.8", "subtle 2.4.1", ] @@ -9982,7 +10018,7 @@ dependencies = [ "paste", "percent-encoding", "rustls 0.19.1", - "sha2 0.10.7", + "sha2 0.10.8", "smallvec", "sqlformat 0.1.8", "sqlx-rt 0.5.13", @@ -10030,7 +10066,7 @@ dependencies = [ "percent-encoding", "rustls 0.20.8", "rustls-pemfile", - "sha2 0.10.7", + "sha2 0.10.8", "smallvec", "sqlformat 0.2.1", "sqlx-rt 0.6.3", @@ -10053,7 +10089,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "sha2 0.10.7", + "sha2 0.10.8", "sqlx-core 0.5.13", "sqlx-rt 0.5.13", "syn 1.0.109", @@ -10072,7 +10108,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "sha2 0.10.7", + "sha2 0.10.8", "sqlx-core 0.6.3", "sqlx-rt 0.6.3", "syn 1.0.109", @@ -10374,7 +10410,7 @@ dependencies = [ "serde_bytes", "serde_json", "serde_repr", - "sha2 0.10.7", + "sha2 0.10.8", "signature 2.1.0", "subtle 2.4.1", "subtle-encoding", @@ -11762,7 +11798,7 @@ dependencies = [ "sdp", "serde", "serde_json", - "sha2 0.10.7", + "sha2 0.10.8", "stun", "thiserror", "time", @@ -11825,7 +11861,7 @@ dependencies = [ "sec1 0.3.0", "serde", "sha1", - "sha2 0.10.7", + "sha2 0.10.8", "signature 1.6.4", "subtle 2.4.1", "thiserror", diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 7439ab8336..e6046f93ca 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -15,6 +15,13 @@ rust-version = "1.56" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +axum = "0.6.20" +sha2 = "0.10.8" +hmac = "0.12.1" +axum-macros = "0.3.8" # Useful for debugging axum Handler trait errors +fastrand = "2" +x25519-dalek = { version = "2.0.0", features = ["static_secrets"] } +base64 = "0.21.4" anyhow = { workspace = true } async-trait = { workspace = true } atty = "0.2" @@ -34,10 +41,21 @@ pretty_env_logger = "0.4" rand = "0.7" serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -sqlx = { version = "0.5", features = [ "runtime-tokio-rustls", "sqlite", "macros", "migrate", ] } +sqlx = { version = "0.5", features = [ + "runtime-tokio-rustls", + "sqlite", + "macros", + "migrate", +] } subtle-encoding = { version = "0.5", features = ["bech32-preview"] } thiserror = "1" -tokio = { workspace = true, features = [ "rt-multi-thread", "net", "signal", "fs", "time" ] } +tokio = { workspace = true, features = [ + "rt-multi-thread", + "net", + "signal", + "fs", + "time", +] } tokio-stream = { version = "0.1.11", features = ["fs"] } tokio-tungstenite = { version = "0.20.1" } tokio-util = { version = "0.7.4", features = ["codec"] } @@ -64,6 +82,11 @@ nym-types = { path = "../common/types" } nym-validator-client = { path = "../common/client-libs/validator-client" } nym-wireguard = { path = "../common/wireguard", optional = true } +[dev-dependencies] +tower = "0.4.13" +rand = "0.8.5" +hyper = "0.14.27" + [build-dependencies] tokio = { version = "1.24.1", features = ["rt-multi-thread", "macros"] } sqlx = { version = "0.5", features = [ diff --git a/gateway/src/error.rs b/gateway/src/error.rs index dde0690979..80fee0a7fa 100644 --- a/gateway/src/error.rs +++ b/gateway/src/error.rs @@ -106,6 +106,16 @@ pub(crate) enum GatewayError { #[from] source: NyxdError, }, + #[error("Error verifying hmac digest")] + HmacDigestError { + #[from] + source: hmac::digest::MacError, + }, + #[error("Invalid hmac length")] + HmacInvalidLength { + #[from] + source: hmac::digest::InvalidLength, + }, } impl From for GatewayError { diff --git a/gateway/src/node/client_handling/client_registration.rs b/gateway/src/node/client_handling/client_registration.rs new file mode 100644 index 0000000000..b8fa5a1cbe --- /dev/null +++ b/gateway/src/node/client_handling/client_registration.rs @@ -0,0 +1,181 @@ +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/client_handling/mod.rs b/gateway/src/node/client_handling/mod.rs index bc63605d58..a5712842f9 100644 --- a/gateway/src/node/client_handling/mod.rs +++ b/gateway/src/node/client_handling/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod active_clients; mod bandwidth; +pub(crate) mod client_registration; pub(crate) mod embedded_network_requester; pub(crate) mod websocket; diff --git a/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs b/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs index 356a884161..32806da385 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs @@ -91,7 +91,7 @@ impl CoconutVerifier { let req = nym_api_requests::coconut::VerifyCredentialBody::new( credential.clone(), proposal_id, - self.nyxd_client.address().clone(), + self.nyxd_client.address(), ); for client in api_clients { self.nyxd_client diff --git a/gateway/src/node/http/api/mod.rs b/gateway/src/node/http/api/mod.rs new file mode 100644 index 0000000000..c0753a12f8 --- /dev/null +++ b/gateway/src/node/http/api/mod.rs @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000000..8e0a63b03a --- /dev/null +++ b/gateway/src/node/http/api/v1/client_registry.rs @@ -0,0 +1,105 @@ +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 new file mode 100644 index 0000000000..082c8dd954 --- /dev/null +++ b/gateway/src/node/http/api/v1/mod.rs @@ -0,0 +1 @@ +pub(crate) mod client_registry; diff --git a/gateway/src/node/http/mod.rs b/gateway/src/node/http/mod.rs new file mode 100644 index 0000000000..9d091f232f --- /dev/null +++ b/gateway/src/node/http/mod.rs @@ -0,0 +1,206 @@ +use std::{collections::HashMap, sync::Arc}; + +use axum::{ + routing::{get, post}, + Router, +}; +use log::info; +use nym_crypto::asymmetric::encryption; +use tokio::sync::RwLock; + +mod api; +use api::v1::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>>, +} + +fn router_with_state(state: Arc) -> 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(); +} + +#[cfg(test)] +mod test { + use std::net::SocketAddr; + use std::str::FromStr; + use std::{collections::HashMap, sync::Arc}; + + use axum::body::Body; + use axum::http::Request; + use axum::http::StatusCode; + use hmac::Mac; + 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 + // 2. Generate DH shared secret + // 3. Client submits its public key to the gateway to start the handshake process, gateway responds with nonce + // 4. Client generates mac digest using DH shared secret, its own public key, socket address and port, and nonce + // 5. Client sends its public key, socket address and port, nonce and mac digest to the gateway + // 6. Gateway verifies mac digest and nonce, and stores client's public key and socket address and port + + let mut rng = rand::thread_rng(); + + let gateway_key_pair = encryption::KeyPair::new(&mut rng); + let client_key_pair = encryption::KeyPair::new(&mut rng); + + let gateway_static_public = + PublicKey::try_from(gateway_key_pair.public_key().to_bytes()).unwrap(); + + let client_static_private = + StaticSecret::try_from(client_key_pair.private_key().to_bytes()).unwrap(); + let client_static_public = + PublicKey::try_from(client_key_pair.public_key().to_bytes()).unwrap(); + + let client_dh = client_static_private.diffie_hellman(&gateway_static_public); + + 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), + }); + + // `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 init_message = + ClientMessage::Init(InitMessage::new(ClientPublicKey::new(client_static_public))); + + let init_request = Request::builder() + .method("POST") + .uri(format!("{}/client", ROUTE_PREFIX)) + .header("Content-type", "application/json") + .body(Body::from(serde_json::to_vec(&init_message).unwrap())) + .unwrap(); + + let response = ServiceExt::>::ready(&mut app) + .await + .unwrap() + .call(init_request) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert!(!registration_in_progress.read().await.is_empty()); + + let nonce: Option = + serde_json::from_slice(&hyper::body::to_bytes(response.into_body()).await.unwrap()) + .unwrap(); + assert!(nonce.is_some()); + + let mut mac = HmacSha256::new_from_slice(client_dh.as_bytes()).unwrap(); + mac.update(client_static_public.as_bytes()); + mac.update("127.0.0.1".as_bytes()); + mac.update("8080".as_bytes()); + 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 final_request = Request::builder() + .method("POST") + .uri(format!("{}/client", ROUTE_PREFIX)) + .header("Content-type", "application/json") + .body(Body::from(serde_json::to_vec(&finalized_message).unwrap())) + .unwrap(); + + let response = ServiceExt::>::ready(&mut app) + .await + .unwrap() + .call(final_request) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert!(!client_registry.read().await.is_empty()); + + let clients_request = Request::builder() + .method("GET") + .uri(format!("{}/clients", ROUTE_PREFIX)) + .body(Body::empty()) + .unwrap(); + + let response = ServiceExt::>::ready(&mut app) + .await + .unwrap() + .call(clients_request) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let clients: Vec = + serde_json::from_slice(&hyper::body::to_bytes(response.into_body()).await.unwrap()) + .unwrap(); + + assert!(!clients.is_empty()); + + let ro_clients = client_registry.read().await.clone(); + assert_eq!( + ro_clients + .values() + .map(|c| c.pub_key().clone()) + .collect::>(), + clients + ) + } +} diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 6a59b1e3e7..132671c8a1 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -1,6 +1,7 @@ // 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; @@ -12,6 +13,7 @@ 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; @@ -27,13 +29,16 @@ use nym_task::{TaskClient, TaskManager}; use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; use rand::seq::SliceRandom; use rand::thread_rng; +use std::collections::HashMap; use std::error::Error; use std::net::SocketAddr; use std::path::PathBuf; use std::sync::Arc; +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; @@ -85,6 +90,8 @@ pub(crate) struct Gateway { /// x25519 keypair used for Diffie-Hellman. Currently only used for sphinx key derivation. sphinx_keypair: Arc, storage: St, + + client_registry: Arc>, } impl Gateway { @@ -100,6 +107,7 @@ impl Gateway { sphinx_keypair: Arc::new(helpers::load_sphinx_keys(&config)?), config, network_requester_opts, + client_registry: Arc::new(RwLock::new(HashMap::new())), }) } @@ -117,6 +125,7 @@ impl Gateway { identity_keypair: Arc::new(identity_keypair), sphinx_keypair: Arc::new(sphinx_keypair), storage, + client_registry: Arc::new(RwLock::new(HashMap::new())), } } @@ -375,6 +384,12 @@ 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/service-providers/network-requester/src/core.rs b/service-providers/network-requester/src/core.rs index 0bad4c36e7..fcc4cb1a59 100644 --- a/service-providers/network-requester/src/core.rs +++ b/service-providers/network-requester/src/core.rs @@ -186,7 +186,7 @@ impl NRServiceProviderBuilder { allowed_hosts::HostsStore::new(&config.storage_paths.unknown_list_location); let outbound_request_filter = - OutboundRequestFilter::new(allowed_hosts.clone(), standard_list.clone(), unknown_hosts); + OutboundRequestFilter::new(allowed_hosts, standard_list, unknown_hosts); NRServiceProviderBuilder { config,