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
This commit is contained in:
@@ -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/<pub-key>`, `clients` and `register` routes to the gateway ([#3955])
|
||||
|
||||
## [2023.1-milka] (2023-09-24)
|
||||
|
||||
|
||||
Generated
+64
-28
@@ -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",
|
||||
|
||||
+25
-2
@@ -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 = [
|
||||
|
||||
@@ -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<ClientCoreError> for GatewayError {
|
||||
|
||||
@@ -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<Sha256>;
|
||||
|
||||
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<u8>);
|
||||
|
||||
impl ClientMac {
|
||||
#[allow(dead_code)]
|
||||
pub fn new(mac: Vec<u8>) -> Self {
|
||||
ClientMac(mac)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ClientMac {
|
||||
type Target = Vec<u8>;
|
||||
|
||||
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<H: Hasher>(&self, state: &mut H) {
|
||||
self.0.as_bytes().hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ClientMac {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mac_bytes: Vec<u8> = 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<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
let encoded_key = general_purpose::STANDARD.encode(self.0.clone());
|
||||
serializer.serialize_str(&encoded_key)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ClientMac {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
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<Self, Self::Err> {
|
||||
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<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
let encoded_key = general_purpose::STANDARD.encode(self.0.as_bytes());
|
||||
serializer.serialize_str(&encoded_key)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ClientPublicKey {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let encoded_key = String::deserialize(deserializer)?;
|
||||
Ok(ClientPublicKey::from_str(&encoded_key).map_err(serde::de::Error::custom))?
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type ClientRegistry = HashMap<SocketAddr, Client>;
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub(crate) mod v1;
|
||||
@@ -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<ApiState>) -> 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<ApiState>) -> 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<Arc<ApiState>>,
|
||||
Json(payload): Json<ClientMessage>,
|
||||
) -> (StatusCode, Json<Option<u64>>) {
|
||||
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<Arc<ApiState>>,
|
||||
) -> (StatusCode, Json<Vec<ClientPublicKey>>) {
|
||||
let registry_ro = state.client_registry.read().await;
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(
|
||||
registry_ro
|
||||
.values()
|
||||
.map(|c| c.pub_key().clone())
|
||||
.collect::<Vec<ClientPublicKey>>(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_client(
|
||||
Path(pub_key): Path<String>,
|
||||
State(state): State<Arc<ApiState>>,
|
||||
) -> (StatusCode, Json<Vec<Client>>) {
|
||||
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::<Vec<Client>>();
|
||||
if clients.is_empty() {
|
||||
return (StatusCode::NOT_FOUND, Json(clients));
|
||||
}
|
||||
(StatusCode::OK, Json(clients))
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub(crate) mod client_registry;
|
||||
@@ -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<RwLock<ClientRegistry>>,
|
||||
sphinx_key_pair: Arc<encryption::KeyPair>,
|
||||
registration_in_progress: Arc<RwLock<HashMap<ClientPublicKey, u64>>>,
|
||||
}
|
||||
|
||||
fn router_with_state(state: Arc<ApiState>) -> 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<RwLock<ClientRegistry>>,
|
||||
sphinx_key_pair: Arc<encryption::KeyPair>,
|
||||
) {
|
||||
// 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<Request<Body>>` 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::<Request<Body>>::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<u64> =
|
||||
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::<Request<Body>>::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::<Request<Body>>::ready(&mut app)
|
||||
.await
|
||||
.unwrap()
|
||||
.call(clients_request)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let clients: Vec<ClientPublicKey> =
|
||||
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::<Vec<ClientPublicKey>>(),
|
||||
clients
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<St = PersistentStorage> {
|
||||
/// x25519 keypair used for Diffie-Hellman. Currently only used for sphinx key derivation.
|
||||
sphinx_keypair: Arc<encryption::KeyPair>,
|
||||
storage: St,
|
||||
|
||||
client_registry: Arc<RwLock<ClientRegistry>>,
|
||||
}
|
||||
|
||||
impl<St> Gateway<St> {
|
||||
@@ -100,6 +107,7 @@ impl<St> Gateway<St> {
|
||||
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<St> Gateway<St> {
|
||||
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<St> Gateway<St> {
|
||||
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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user