Compare commits

..

32 Commits

Author SHA1 Message Date
Tommy Verrall 7abe9a987d PR comments addressed
- Changed peer_interaction_timeout from u64 milliseconds to Duration type
- Made timeout non-configurable (always uses safe 5s default) to prevent
  users from setting unsafe values
- Removed timeout from CLI args, env vars, and config template
- Added suspend/resume scenario documentation
- Simplified code by removing unnecessary tuple
- Reverted old_config migration code (to be handled in separate PR)
2025-12-11 14:46:16 +01:00
Tommy Verrall 24ccba3a19 expose peer timeout config + update migration 2025-11-21 16:34:25 +01:00
Tommy Verrall 21a1376900 drop registration lock and enhance gateway reservation 2025-11-21 12:25:03 +01:00
Tommy Verrall 451107efb5 add configurable peer-controller timeout and make registration lockless 2025-11-21 09:57:20 +01:00
Tommy Verrall 8dbbd2bdf4 gateway/authenticator: add peer-controller timeouts and lock tweaks to keep registration responsive after suspend 2025-11-21 09:26:41 +01:00
Mark Sinclair 96e3ff2af9 Node Status UI (#6210)
Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
2025-11-17 09:18:01 +00:00
Jędrzej Stuczyński d73b7b7127 chore: remove support for legacy mixnode within the performance contract (#6205) 2025-11-14 15:04:59 +00:00
Jędrzej Stuczyński 440aadf124 chore: updated default endpoint for retrieving attestation.json (#6207) 2025-11-14 15:04:51 +00:00
Jędrzej Stuczyński d126d8e5a0 feat: upgrade mode: VPN adjustments (#6189)
* placeholder handling of wg registration with upgrade mode token

* include upgrade mode credentials as part of credential storage

* introduce helper for decoding JWT payload

* expose methods for removing emergency credentials from the storage

* don't allow duplicate emergency credentials with the same content

* added authenticator ClientMessage for upgrade mode check

* retrieve credentials with longest expiration first

* post rebasing fixes

* fixed gateway config

* feat: allow specifying minimum node performance for client init

* nym-node UM improvements

* fixed upgrade mode bandwidth on initial authentication

* fix: logs and thresholds

* expose attestation information from nym-node http api

* additional logs

* post rebasing fixes

* make @simonwicky happy by removing empty lines in emergency_credential table definition

* chore: remove '_' prefix for internal counters within in-mem ecash storage

* improved import of 'UpgradeModeState' within the nym-node

* use explicit time dependency within credential-storage

* re-order imports within the gateway-client

* moved 'AvailableBandwidth' definition to the monorepo
2025-11-14 13:34:36 +00:00
Jędrzej Stuczyński 6b2bb3029b feat: merge intermediate upgrade mode changes (#6174)
* squashing feat: merge intermediate upgrade mode changes #6174 to more easily resolve merge conflicts during rebasing

added additional v2 query for metadata endpoint for requesting upgrade mode recheck

added additional message to v6 authenticator to request explicit upgrade mode recheck

clippy

test fixes due to updated keys

updated assertion for upgrading v1 top up request to v2

compare attester public key against the expected value within the credential proxy

use pre-generated attestation public keys within nym-nodes

remove version deprecation

bugfix: default bandwidth response for authenticator

expose upgrade mode information in authenticator responses

adding tests for new v2 server

passing upgrade mode information in metadata endpoint

v2 wireguard private metadata

bugfix: make sure to immediately poll for attestation after spawning task

fix gateway probe and remove code duplication for finalizing registration

squashing before rebasing

post rebasing fixes

AuthenticatorVersion helpers

additional nits

allow unwraps in mocks

fixed linux build

clippy

integrating upgrade mode into authenticator

fixed build after adding wrappers to response types

conditionally updating peer handle bandwidth

cleanup

negotiate initial protocol during registration

change auth to use highest protocol

handler for JWT message

dont meter client bandwidth in upgrade mode

handling recheck requests

sending information about upgrade_mode on client messages

gateway watching for upgrade mode attestation

wip: gateways to disable bandwidth metering on upgrade mode

* fixed ServerResponse deserialisation

* fixed incorrect swagger path for upgrade mode check endpoint

* moved upgrade mode endpoint out of bandwidth routes

* chore: remove unused error variant

* removed re-export of UpgradeModeAttestation from credentials-interface

* chore: define single source of truth for minimum bandwidth threshold value

* moved type definitions out of traits.rs

* updated v6 versioning to point to niolo release instead

* fixed incorrect error mapping
2025-11-14 13:13:15 +00:00
benedetta davico 4dcc568ec2 Merge pull request #6204 from nymtech/master
merging master to develop to maintain sync
2025-11-14 11:21:13 +01:00
benedetta davico 468835e3a2 Merge pull request #6199 from nymtech/release/2025.20-leerdammer
Release/2025.20 leerdammer
2025-11-14 10:36:23 +01:00
benedetta davico 28a866e26d Merge pull request #6198 from nymtech/release/2025.20-leerdammer
Release/2025.20 leerdammer
2025-11-14 10:36:11 +01:00
Jędrzej Stuczyński 350d244032 bugfix: fix credential proxy upgrade mode attestation url arg (#6202)
this includes bringing over changes introduced in #6174
2025-11-14 08:19:21 +00:00
Jack Wampler 17ca000782 HTTP API resilience enable & domain rotation conditions (#6200)
* http url fallback conditions

* include changes and tests for fronted

* Allow for explicit DNS error Handling in HTTP client  (#6201)

when sending http reqs add manual DNS so we can handle errors directly

* Address PR nits

---------

Co-authored-by: durch <durch@users.noreply.github.com>
2025-11-14 08:59:36 +01:00
Drazen Urch aac983d922 Remove debug feature from http-macro spec in gateway probe (#6195) 2025-11-13 14:18:29 +01:00
mfahampshire 577675bab3 Remove old conceptsoverview page + move index to proper place in sidebar (#6196) 2025-11-13 11:38:54 +00:00
mfahampshire ec015618cd update gw probe to point @ monorepo (#6194)
* update gw probe to point @ monorepo

* add funded nyx account info
2025-11-13 11:02:45 +00:00
mfahampshire fa40acbeca fixed broken link (#6193) 2025-11-12 15:12:38 +00:00
import this 386e1790dd [DOCs/operators]: Release notes for v2025.20 leerdammer (#6191)
* release notes

* bump up nym-node docs version

* add dev tools

* scrape stats and clean
2025-11-12 12:32:13 +00:00
mfahampshire d07f9c8fad Max/docs new structure (#6188)
* rework of sdk docs

* update integration docs + bit of overall restructure

* remove debug logger from tool
2025-11-12 11:03:28 +00:00
Tommy Verrall 0dc071daeb Merge pull request #6179 from nymtech/dns-debug
DNS relibility and troubleshooting
2025-11-12 11:01:20 +01:00
benedettadavico babf113fe5 update changelog 2025-11-12 08:39:48 +01:00
jmwample 10951d4cd3 clippy, fmt, minor fix 2025-11-11 10:40:25 -07:00
mfahampshire 872c25bfcc Use hardcoded devrel gw for examples to get around CSP (#6187)
* Use hardcoded devrel gw for examples to get around CSP

* remove comment
2025-11-11 16:22:41 +00:00
jmwample 5acce42c64 add some staic hosts and switch server strategy 2025-11-11 09:14:26 -07:00
mfahampshire 4848d081d0 Max/tweak ts sdk actions (#6185)
* add taskset to wasm release build commands

* bump taskset cores
2025-11-11 10:19:55 +00:00
mfahampshire b3452ede77 add wss to prod csp (#6183)
* add wss to csp
2025-11-10 20:48:02 +00:00
jmwample 2f752a6c42 fix things related to interface changes 2025-11-06 18:37:50 -07:00
Jack Wampler 806f807f02 Implement Static DNS fallback (#6178) 2025-11-06 16:46:39 -07:00
Jack Wampler 1400db6156 DNS Reliability Fixes (#6175) 2025-11-06 12:37:27 -07:00
benedettadavico 0d7487f530 bump versions 2025-10-31 13:24:27 +01:00
345 changed files with 16218 additions and 3289 deletions
+2 -2
View File
@@ -23,7 +23,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup yarn
run: npm install -g yarn
@@ -54,6 +54,6 @@ jobs:
- name: Lint
run: yarn lint
- name: Typecheck with tsc
run: yarn tsc
+32
View File
@@ -4,6 +4,38 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2025.20-leerdammer] (2025-11-12)
- Max/tweak ts sdk actions ([#6185])
- chore: resolve clippy 1.91 warnings ([#6168])
- [chore] Remove unused dependencies ([#6151])
- Use typed-builder for registration client builder config ([#6150])
- tommy is too quick ([#6149])
- configurable mixnet client startup timeout ([#6148])
- [Feature/operators]: QUIC bridge deployment script v2 ([#6145])
- Bugfix: Add circuit breaker ([#6143])
- bugfix: update internal owner address in transferred share ([#6139])
- Update quic_bridge_deployment.sh for IPv4 and .deb package ([#6138])
- feat: expose more explicit new_with_fronted_urls builder for http API client ([#6136])
- bugfix: update stored epoch share when changing ownership ([#6135])
- Domain fronting ([#6134])
- bugfix: update stored epoch share when changing announce address ([#6131])
[#6185]: https://github.com/nymtech/nym/pull/6185
[#6168]: https://github.com/nymtech/nym/pull/6168
[#6151]: https://github.com/nymtech/nym/pull/6151
[#6150]: https://github.com/nymtech/nym/pull/6150
[#6149]: https://github.com/nymtech/nym/pull/6149
[#6148]: https://github.com/nymtech/nym/pull/6148
[#6145]: https://github.com/nymtech/nym/pull/6145
[#6143]: https://github.com/nymtech/nym/pull/6143
[#6139]: https://github.com/nymtech/nym/pull/6139
[#6138]: https://github.com/nymtech/nym/pull/6138
[#6136]: https://github.com/nymtech/nym/pull/6136
[#6135]: https://github.com/nymtech/nym/pull/6135
[#6134]: https://github.com/nymtech/nym/pull/6134
[#6131]: https://github.com/nymtech/nym/pull/6131
## [2025.19-kase] (2025-10-30)
- update ns agent workflow ([#6154])
Generated
+24 -43
View File
@@ -4825,7 +4825,7 @@ dependencies = [
[[package]]
name = "nym-api"
version = "1.1.68"
version = "1.1.69"
dependencies = [
"anyhow",
"async-trait",
@@ -4988,6 +4988,7 @@ dependencies = [
"nym-network-defaults",
"nym-service-provider-requests-common",
"nym-sphinx",
"nym-test-utils",
"nym-wireguard-types",
"rand 0.8.5",
"semver 1.0.26",
@@ -4995,6 +4996,7 @@ dependencies = [
"sha2 0.10.9",
"strum_macros",
"thiserror 2.0.12",
"tracing",
"x25519-dalek",
]
@@ -5003,21 +5005,16 @@ name = "nym-bandwidth-controller"
version = "0.1.0"
dependencies = [
"async-trait",
"bip39",
"log",
"nym-credential-storage",
"nym-credentials",
"nym-credentials-interface",
"nym-crypto",
"nym-ecash-contract-common",
"nym-ecash-time",
"nym-network-defaults",
"nym-task",
"nym-validator-client",
"rand 0.8.5",
"thiserror 2.0.12",
"url",
"zeroize",
]
[[package]]
@@ -5051,7 +5048,7 @@ dependencies = [
[[package]]
name = "nym-cli"
version = "1.1.65"
version = "1.1.66"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -5134,7 +5131,7 @@ dependencies = [
[[package]]
name = "nym-client"
version = "1.1.65"
version = "1.1.66"
dependencies = [
"bs58",
"clap",
@@ -5570,6 +5567,7 @@ dependencies = [
"sqlx",
"sqlx-pool-guard",
"thiserror 2.0.12",
"time",
"tokio",
"zeroize",
]
@@ -5605,12 +5603,13 @@ dependencies = [
"nym-api-requests",
"nym-credentials",
"nym-credentials-interface",
"nym-crypto",
"nym-ecash-contract-common",
"nym-gateway-requests",
"nym-gateway-storage",
"nym-task",
"nym-upgrade-mode-check",
"nym-validator-client",
"rand 0.8.5",
"si-scale",
"thiserror 2.0.12",
"time",
@@ -5650,6 +5649,7 @@ dependencies = [
"nym-compact-ecash",
"nym-ecash-time",
"nym-network-defaults",
"nym-upgrade-mode-check",
"rand 0.8.5",
"serde",
"strum",
@@ -5800,7 +5800,6 @@ dependencies = [
name = "nym-gateway"
version = "1.1.36"
dependencies = [
"anyhow",
"async-trait",
"bincode",
"bip39",
@@ -5811,7 +5810,6 @@ dependencies = [
"futures",
"ipnetwork",
"mock_instant",
"nym-api-requests",
"nym-authenticator-requests",
"nym-client-core",
"nym-credential-verification",
@@ -5824,7 +5822,6 @@ dependencies = [
"nym-id",
"nym-ip-packet-router",
"nym-mixnet-client",
"nym-mixnode-common",
"nym-network-defaults",
"nym-network-requester",
"nym-node-metrics",
@@ -5834,20 +5831,18 @@ dependencies = [
"nym-statistics-common",
"nym-task",
"nym-topology",
"nym-types",
"nym-upgrade-mode-check",
"nym-validator-client",
"nym-wireguard",
"nym-wireguard-private-metadata-server",
"nym-wireguard-types",
"rand 0.8.5",
"serde",
"sha2 0.10.9",
"thiserror 2.0.12",
"time",
"tokio",
"tokio-stream",
"tokio-tungstenite",
"tokio-util",
"tracing",
"url",
"zeroize",
@@ -6058,6 +6053,7 @@ dependencies = [
"thiserror 2.0.12",
"tokio",
"tracing",
"tracing-subscriber",
"url",
"wasmtimer",
]
@@ -6362,7 +6358,7 @@ dependencies = [
[[package]]
name = "nym-network-requester"
version = "1.1.66"
version = "1.1.67"
dependencies = [
"addr",
"anyhow",
@@ -6412,7 +6408,7 @@ dependencies = [
[[package]]
name = "nym-node"
version = "1.20.0"
version = "1.21.0"
dependencies = [
"anyhow",
"arc-swap",
@@ -6436,6 +6432,7 @@ dependencies = [
"futures",
"hkdf",
"human-repr",
"humantime",
"humantime-serde",
"indicatif",
"ipnetwork",
@@ -6443,6 +6440,7 @@ dependencies = [
"nym-bin-common",
"nym-client-core-config-types",
"nym-config",
"nym-credential-verification",
"nym-crypto",
"nym-gateway",
"nym-gateway-stats-storage",
@@ -6515,13 +6513,13 @@ version = "0.1.0"
dependencies = [
"async-trait",
"celes",
"humantime",
"humantime-serde",
"nym-bin-common",
"nym-crypto",
"nym-exit-policy",
"nym-http-api-client",
"nym-noise-keys",
"nym-upgrade-mode-check",
"nym-wireguard-types",
"rand_chacha 0.3.1",
"schemars 0.8.22",
@@ -6532,6 +6530,7 @@ dependencies = [
"thiserror 2.0.12",
"time",
"tokio",
"url",
"utoipa",
]
@@ -6939,7 +6938,7 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "1.1.65"
version = "1.1.66"
dependencies = [
"bs58",
"clap",
@@ -7573,17 +7572,10 @@ dependencies = [
name = "nym-wireguard"
version = "0.1.0"
dependencies = [
"async-trait",
"base64 0.22.1",
"bincode",
"chrono",
"dashmap",
"defguard_wireguard_rs",
"dyn-clone",
"futures",
"ip_network",
"log",
"nym-authenticator-requests",
"nym-credential-verification",
"nym-credentials-interface",
"nym-crypto",
@@ -7594,11 +7586,9 @@ dependencies = [
"nym-task",
"nym-wireguard-types",
"thiserror 2.0.12",
"time",
"tokio",
"tokio-stream",
"tracing",
"x25519-dalek",
]
[[package]]
@@ -7650,15 +7640,20 @@ version = "1.0.0"
dependencies = [
"async-trait",
"axum",
"futures",
"nym-credential-verification",
"nym-credentials-interface",
"nym-crypto",
"nym-http-api-client",
"nym-http-api-common",
"nym-upgrade-mode-check",
"nym-wireguard",
"nym-wireguard-private-metadata-client",
"nym-wireguard-private-metadata-server",
"nym-wireguard-private-metadata-shared",
"time",
"tokio",
"tower 0.5.2",
"tower-http 0.5.2",
"utoipa",
]
@@ -7668,10 +7663,7 @@ name = "nym-wireguard-types"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"log",
"nym-config",
"nym-crypto",
"nym-network-defaults",
"rand 0.8.5",
"serde",
"thiserror 2.0.12",
@@ -7680,7 +7672,7 @@ dependencies = [
[[package]]
name = "nymvisor"
version = "0.1.30"
version = "0.1.31"
dependencies = [
"anyhow",
"bytes",
@@ -8601,17 +8593,6 @@ dependencies = [
"psl-types",
]
[[package]]
name = "query-tester"
version = "0.1.0"
dependencies = [
"anyhow",
"nym-network-defaults",
"nym-validator-client",
"serde",
"tokio",
]
[[package]]
name = "quick-error"
version = "2.0.1"
+2 -1
View File
@@ -166,11 +166,12 @@ members = [
"wasm/node-tester",
"wasm/zknym-lib",
"nym-gateway-probe"
, "query-tester"]
]
default-members = [
"clients/native",
"clients/socks5",
"nym-authenticator-client",
"nym-api",
"nym-credential-proxy/nym-credential-proxy",
"nym-node",
+1 -2
View File
@@ -140,8 +140,7 @@ clippy: sdk-wasm-lint
WASM_CONTRACT_DIR := contracts/target/wasm32-unknown-unknown/release
# Find every direct contract folder that contains a Cargo.toml
#CONTRACT_DIRS := $(shell find contracts -type f -name Cargo.toml \( ! -path "contracts/Cargo.toml" \) | grep -v integration-tests | xargs -n1 dirname | sort -u)
CONTRACT_DIRS := contracts/example-contract
CONTRACT_DIRS := $(shell find contracts -type f -name Cargo.toml \( ! -path "contracts/Cargo.toml" \) | grep -v integration-tests | xargs -n1 dirname | sort -u)
CONTRACTS_OUT_DIR = contracts/artifacts
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.65"
version = "1.1.66"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.65"
version = "1.1.66"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
+7
View File
@@ -16,6 +16,7 @@ serde = { workspace = true, features = ["derive"] }
semver = { workspace = true }
strum_macros = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
nym-credentials-interface = { path = "../credentials-interface" }
nym-crypto = { path = "../crypto", features = ["asymmetric"] }
@@ -29,7 +30,13 @@ hmac = { workspace = true, optional = true }
sha2 = { workspace = true, optional = true }
x25519-dalek = { workspace = true, features = ["static_secrets"] }
[dev-dependencies]
nym-test-utils = { path = "../test-utils" }
[features]
default = ["verify"]
# this is moved to a separate feature as we really need clients to import it (especially, *cough*, wasm)
verify = ["hmac", "sha2"]
[lints]
workspace = true
@@ -6,9 +6,11 @@ use nym_wireguard_types::PeerPublicKey;
use crate::{
AuthenticatorVersion, Error,
latest::registration::IpPair,
traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, Versionable},
v2, v3, v4, v5,
traits::{
FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, UpgradeModeMessage,
Versionable,
},
v2, v3, v4, v5, v6,
};
// This is very redundant with AuthenticatorRequest and I reckon they could be smooshed.
@@ -19,6 +21,293 @@ pub enum ClientMessage {
Final(Box<dyn FinalMessage + Send + Sync + 'static>),
Query(Box<dyn QueryBandwidthMessage + Send + Sync + 'static>),
TopUp(Box<dyn TopUpMessage + Send + Sync + 'static>),
UpgradeModeCheck(Box<dyn UpgradeModeMessage + Send + Sync + 'static>),
}
pub struct SerialisedRequest {
pub bytes: Vec<u8>,
pub request_id: u64,
}
impl SerialisedRequest {
pub fn new(bytes: Vec<u8>, request_id: u64) -> Self {
Self { bytes, request_id }
}
}
impl ClientMessage {
fn serialise_v1(&self) -> Result<SerialisedRequest, Error> {
Err(Error::UnsupportedVersion)
}
fn serialise_v2(&self, reply_to: Recipient) -> Result<SerialisedRequest, Error> {
use v2::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage},
request::AuthenticatorRequest,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(
InitMessage {
pub_key: init_message.pub_key(),
},
reply_to,
);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(
FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ip: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?
.into(),
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message
.credential()
.and_then(|c| c.credential.into_zk_nym())
.map(|c| *c),
},
reply_to,
);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) =
AuthenticatorRequest::new_query_request(query_message.pub_key(), reply_to);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
_ => Err(Error::UnsupportedMessage),
}
}
fn serialise_v3(&self, reply_to: Recipient) -> Result<SerialisedRequest, Error> {
use v3::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage},
request::AuthenticatorRequest,
topup::TopUpMessage,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(
InitMessage {
pub_key: init_message.pub_key(),
},
reply_to,
);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(
FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ip: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?
.into(),
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message
.credential()
.and_then(|c| c.credential.into_zk_nym())
.map(|c| *c),
},
reply_to,
);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) =
AuthenticatorRequest::new_query_request(query_message.pub_key(), reply_to);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::TopUp(top_up_message) => {
let (req, id) = AuthenticatorRequest::new_topup_request(
TopUpMessage {
pub_key: top_up_message.pub_key(),
credential: top_up_message.credential(),
},
reply_to,
);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
_ => Err(Error::UnsupportedMessage),
}
}
fn serialise_v4(&self, reply_to: Recipient) -> Result<SerialisedRequest, Error> {
use v4::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair},
request::AuthenticatorRequest,
topup::TopUpMessage,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(
InitMessage {
pub_key: init_message.pub_key(),
},
reply_to,
);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(
FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ips: IpPair {
ipv4: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?,
ipv6: final_message
.gateway_client_ipv6()
.ok_or(Error::UnsupportedMessage)?,
},
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message
.credential()
.and_then(|c| c.credential.into_zk_nym())
.map(|c| *c),
},
reply_to,
);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) =
AuthenticatorRequest::new_query_request(query_message.pub_key(), reply_to);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::TopUp(top_up_message) => {
let (req, id) = AuthenticatorRequest::new_topup_request(
TopUpMessage {
pub_key: top_up_message.pub_key(),
credential: top_up_message.credential(),
},
reply_to,
);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
_ => Err(Error::UnsupportedMessage),
}
}
fn serialise_v5(&self) -> Result<SerialisedRequest, Error> {
use v5::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair},
request::AuthenticatorRequest,
topup::TopUpMessage,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(InitMessage {
pub_key: init_message.pub_key(),
});
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ips: IpPair {
ipv4: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?,
ipv6: final_message
.gateway_client_ipv6()
.ok_or(Error::UnsupportedMessage)?,
},
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message
.credential()
.and_then(|c| c.credential.into_zk_nym())
.map(|c| *c),
});
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) = AuthenticatorRequest::new_query_request(query_message.pub_key());
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::TopUp(top_up_message) => {
let (req, id) = AuthenticatorRequest::new_topup_request(TopUpMessage {
pub_key: top_up_message.pub_key(),
credential: top_up_message.credential(),
});
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
_ => Err(Error::UnsupportedMessage),
}
}
fn serialise_v6(&self) -> Result<SerialisedRequest, Error> {
use v6::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair},
request::AuthenticatorRequest,
topup::TopUpMessage,
upgrade_mode_check::UpgradeModeCheckRequest,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(InitMessage {
pub_key: init_message.pub_key(),
});
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ips: IpPair {
ipv4: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?,
ipv6: final_message
.gateway_client_ipv6()
.ok_or(Error::UnsupportedMessage)?,
},
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message.credential(),
});
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) = AuthenticatorRequest::new_query_request(query_message.pub_key());
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::TopUp(top_up_message) => {
let (req, id) = AuthenticatorRequest::new_topup_request(TopUpMessage {
pub_key: top_up_message.pub_key(),
credential: top_up_message.credential(),
});
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::UpgradeModeCheck(upgrade_mode_check) => {
// currently JWT is the only emergency credential option
let Some(upgrade_mode_jwt) =
upgrade_mode_check.upgrade_mode_global_attestation_jwt()
else {
return Err(Error::conversion(
"no valid known upgrade mode check variants",
));
};
let msg = UpgradeModeCheckRequest::UpgradeModeJwt {
token: upgrade_mode_jwt,
};
let (req, id) = AuthenticatorRequest::new_upgrade_mode_check_request(msg);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
}
}
}
impl ClientMessage {
@@ -27,7 +316,7 @@ impl ClientMessage {
match self {
Self::Final(msg) => msg.credential().is_some(),
Self::TopUp(_) => true,
Self::Initial(_) | Self::Query(_) => false,
Self::Initial(_) | Self::Query(_) | Self::UpgradeModeCheck(_) => false,
}
}
@@ -37,208 +326,18 @@ impl ClientMessage {
ClientMessage::Final(msg) => msg.version(),
ClientMessage::Query(msg) => msg.version(),
ClientMessage::TopUp(msg) => msg.version(),
ClientMessage::UpgradeModeCheck(msg) => msg.version(),
}
}
pub fn bytes(&self, reply_to: Recipient) -> Result<(Vec<u8>, u64), Error> {
pub fn bytes(&self, reply_to: Recipient) -> Result<SerialisedRequest, Error> {
match self.version() {
AuthenticatorVersion::V1 => Err(Error::UnsupportedVersion),
AuthenticatorVersion::V2 => {
use v2::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage},
request::AuthenticatorRequest,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(
InitMessage {
pub_key: init_message.pub_key(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(
FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ip: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?
.into(),
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message.credential(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) = AuthenticatorRequest::new_query_request(
query_message.pub_key(),
reply_to,
);
Ok((req.to_bytes()?, id))
}
_ => Err(Error::UnsupportedMessage),
}
}
AuthenticatorVersion::V3 => {
use v3::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage},
request::AuthenticatorRequest,
topup::TopUpMessage,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(
InitMessage {
pub_key: init_message.pub_key(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(
FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ip: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?
.into(),
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message.credential(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) = AuthenticatorRequest::new_query_request(
query_message.pub_key(),
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::TopUp(top_up_message) => {
let (req, id) = AuthenticatorRequest::new_topup_request(
TopUpMessage {
pub_key: top_up_message.pub_key(),
credential: top_up_message.credential(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
}
}
AuthenticatorVersion::V4 => {
use v4::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage},
request::AuthenticatorRequest,
topup::TopUpMessage,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(
InitMessage {
pub_key: init_message.pub_key(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(
FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ips: IpPair {
ipv4: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?,
ipv6: final_message
.gateway_client_ipv6()
.ok_or(Error::UnsupportedMessage)?,
}
.into(),
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message.credential(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) = AuthenticatorRequest::new_query_request(
query_message.pub_key(),
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::TopUp(top_up_message) => {
let (req, id) = AuthenticatorRequest::new_topup_request(
TopUpMessage {
pub_key: top_up_message.pub_key(),
credential: top_up_message.credential(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
}
}
AuthenticatorVersion::V5 => {
use v5::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage},
request::AuthenticatorRequest,
topup::TopUpMessage,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(InitMessage {
pub_key: init_message.pub_key(),
});
Ok((req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ips: IpPair {
ipv4: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?,
ipv6: final_message
.gateway_client_ipv6()
.ok_or(Error::UnsupportedMessage)?,
},
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message.credential(),
});
Ok((req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) =
AuthenticatorRequest::new_query_request(query_message.pub_key());
Ok((req.to_bytes()?, id))
}
ClientMessage::TopUp(top_up_message) => {
let (req, id) = AuthenticatorRequest::new_topup_request(TopUpMessage {
pub_key: top_up_message.pub_key(),
credential: top_up_message.credential(),
});
Ok((req.to_bytes()?, id))
}
}
}
AuthenticatorVersion::V1 => self.serialise_v1(),
AuthenticatorVersion::V2 => self.serialise_v2(reply_to),
AuthenticatorVersion::V3 => self.serialise_v3(reply_to),
AuthenticatorVersion::V4 => self.serialise_v4(reply_to),
AuthenticatorVersion::V5 => self.serialise_v5(),
AuthenticatorVersion::V6 => self.serialise_v6(),
AuthenticatorVersion::UNKNOWN => Err(Error::UnknownVersion),
}
}
@@ -247,7 +346,7 @@ impl ClientMessage {
use AuthenticatorVersion::*;
match self.version() {
V1 | V2 | V3 | V4 => false,
V5 => true,
V5 | V6 => true,
UNKNOWN => true,
}
}
@@ -1,6 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::fmt::Display;
use thiserror::Error;
#[derive(Debug, Error)]
@@ -37,3 +38,13 @@ pub enum Error {
#[error(transparent)]
Bincode(#[from] bincode::Error),
}
impl Error {
pub fn conversion(msg: impl Into<String>) -> Self {
Error::Conversion(msg.into())
}
pub fn conversion_display(msg: impl Display) -> Self {
Error::Conversion(msg.to_string())
}
}
+3 -1
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
pub mod client_message;
pub mod models;
pub mod request;
pub mod response;
pub mod traits;
@@ -10,13 +11,14 @@ pub mod v2;
pub mod v3;
pub mod v4;
pub mod v5;
pub mod v6;
mod error;
mod util;
mod version;
pub use error::Error;
pub use v5 as latest;
pub use v6 as latest;
pub use version::AuthenticatorVersion;
pub const CURRENT_VERSION: u8 = latest::VERSION;
@@ -0,0 +1,58 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_credentials_interface::{
BandwidthCredential, CredentialSpendingData, TicketType, UnknownTicketType,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
pub enum CurrentUpgradeModeStatus {
Enabled,
Disabled,
// everything pre-v6
Unknown,
}
impl CurrentUpgradeModeStatus {
pub fn is_enabled(&self) -> bool {
matches!(self, CurrentUpgradeModeStatus::Enabled)
}
}
impl From<bool> for CurrentUpgradeModeStatus {
fn from(value: bool) -> Self {
if value {
CurrentUpgradeModeStatus::Enabled
} else {
CurrentUpgradeModeStatus::Disabled
}
}
}
impl From<CurrentUpgradeModeStatus> for Option<bool> {
fn from(value: CurrentUpgradeModeStatus) -> Self {
match value {
CurrentUpgradeModeStatus::Enabled => Some(true),
CurrentUpgradeModeStatus::Disabled => Some(false),
CurrentUpgradeModeStatus::Unknown => None,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct BandwidthClaim {
pub credential: BandwidthCredential,
pub kind: TicketType,
}
impl TryFrom<CredentialSpendingData> for BandwidthClaim {
type Error = UnknownTicketType;
fn try_from(credential: CredentialSpendingData) -> Result<Self, Self::Error> {
Ok(BandwidthClaim {
kind: TicketType::try_from_encoded(credential.payment.t_type)?,
credential: BandwidthCredential::from(credential),
})
}
}
+51 -2
View File
@@ -4,8 +4,10 @@
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use nym_sphinx::addressing::Recipient;
use crate::traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage};
use crate::{v1, v2, v3, v4, v5};
use crate::traits::{
FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, UpgradeModeMessage,
};
use crate::{v1, v2, v3, v4, v5, v6};
#[derive(Debug)]
pub enum AuthenticatorRequest {
@@ -33,6 +35,11 @@ pub enum AuthenticatorRequest {
reply_to: Option<Recipient>,
request_id: u64,
},
CheckUpgradeMode {
msg: Box<dyn UpgradeModeMessage + Send + Sync + 'static>,
protocol: Protocol,
request_id: u64,
},
}
impl From<v1::request::AuthenticatorRequest> for AuthenticatorRequest {
@@ -202,3 +209,45 @@ impl From<v5::request::AuthenticatorRequest> for AuthenticatorRequest {
}
}
}
impl From<v6::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v6::request::AuthenticatorRequest) -> Self {
match value.data {
v6::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
},
v6::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
},
v6::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
}
}
v6::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => {
Self::TopUpBandwidth {
msg: top_up_message,
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
}
}
v6::request::AuthenticatorRequestData::CheckUpgradeMode(upgrade_mode_check_msg) => {
Self::CheckUpgradeMode {
msg: Box::new(upgrade_mode_check_msg),
protocol: value.protocol,
request_id: value.request_id,
}
}
}
}
}
+49 -2
View File
@@ -1,11 +1,12 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::CurrentUpgradeModeStatus;
use crate::traits::{
Id, PendingRegistrationResponse, RegisteredResponse, RemainingBandwidthResponse,
TopUpBandwidthResponse,
TopUpBandwidthResponse, UpgradeModeStatus,
};
use crate::{v2, v3, v4, v5};
use crate::{v2, v3, v4, v5, v6};
#[derive(Debug)]
pub enum AuthenticatorResponse {
@@ -13,6 +14,29 @@ pub enum AuthenticatorResponse {
Registered(Box<dyn RegisteredResponse + Send + Sync + 'static>),
RemainingBandwidth(Box<dyn RemainingBandwidthResponse + Send + Sync + 'static>),
TopUpBandwidth(Box<dyn TopUpBandwidthResponse + Send + Sync + 'static>),
UpgradeMode(Box<dyn UpgradeModeStatus + Send + Sync + 'static>),
}
impl UpgradeModeStatus for AuthenticatorResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
match self {
AuthenticatorResponse::PendingRegistration(pending_registration_response) => {
pending_registration_response.upgrade_mode_status()
}
AuthenticatorResponse::Registered(registered_response) => {
registered_response.upgrade_mode_status()
}
AuthenticatorResponse::RemainingBandwidth(remaining_bandwidth_response) => {
remaining_bandwidth_response.upgrade_mode_status()
}
AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => {
top_up_bandwidth_response.upgrade_mode_status()
}
AuthenticatorResponse::UpgradeMode(upgrade_mode_response) => {
upgrade_mode_response.upgrade_mode_status()
}
}
}
}
impl Id for AuthenticatorResponse {
@@ -28,6 +52,7 @@ impl Id for AuthenticatorResponse {
AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => {
top_up_bandwidth_response.id()
}
AuthenticatorResponse::UpgradeMode(upgrade_mode_response) => upgrade_mode_response.id(),
}
}
}
@@ -104,3 +129,25 @@ impl From<v5::response::AuthenticatorResponse> for AuthenticatorResponse {
}
}
}
impl From<v6::response::AuthenticatorResponse> for AuthenticatorResponse {
fn from(value: v6::response::AuthenticatorResponse) -> Self {
match value.data {
v6::response::AuthenticatorResponseData::PendingRegistration(
pending_registration_response,
) => Self::PendingRegistration(Box::new(pending_registration_response)),
v6::response::AuthenticatorResponseData::Registered(registered_response) => {
Self::Registered(Box::new(registered_response))
}
v6::response::AuthenticatorResponseData::RemainingBandwidth(
remaining_bandwidth_response,
) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)),
v6::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => {
Self::TopUpBandwidth(Box::new(top_up_bandwidth_response))
}
v6::response::AuthenticatorResponseData::UpgradeMode(upgrade_mode_check_response) => {
Self::UpgradeMode(Box::new(upgrade_mode_check_response))
}
}
}
}
+456 -34
View File
@@ -1,15 +1,15 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::latest::registration::IpPair;
use crate::models::{BandwidthClaim, CurrentUpgradeModeStatus};
use crate::{AuthenticatorVersion, Error, v1, v2, v3, v4, v5, v6};
use nym_credentials_interface::CredentialSpendingData;
use nym_crypto::asymmetric::x25519;
use nym_wireguard_types::PeerPublicKey;
use std::fmt;
use std::net::{Ipv4Addr, Ipv6Addr};
use nym_credentials_interface::CredentialSpendingData;
use nym_crypto::asymmetric::x25519::PrivateKey;
use nym_wireguard_types::PeerPublicKey;
use crate::latest::registration::IpPair;
use crate::{AuthenticatorVersion, Error, v1, v2, v3, v4, v5};
use tracing::error;
pub trait Versionable {
fn version(&self) -> AuthenticatorVersion;
@@ -51,6 +51,12 @@ impl Versionable for v5::registration::InitMessage {
}
}
impl Versionable for v6::registration::InitMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V6
}
}
impl Versionable for v2::registration::FinalMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V2
@@ -75,6 +81,12 @@ impl Versionable for v5::registration::FinalMessage {
}
}
impl Versionable for v6::registration::FinalMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V6
}
}
impl Versionable for PeerPublicKey {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V3
@@ -98,6 +110,158 @@ impl Versionable for v5::topup::TopUpMessage {
AuthenticatorVersion::V5
}
}
impl Versionable for v6::topup::TopUpMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V6
}
}
impl Versionable for v6::upgrade_mode_check::UpgradeModeCheckRequest {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V6
}
}
pub trait UpgradeModeStatus: Id + fmt::Debug {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus;
}
impl UpgradeModeStatus for v1::response::PendingRegistrationResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v1::response::RegisteredResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v1::response::RemainingBandwidthResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v2::response::PendingRegistrationResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v2::response::RegisteredResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v2::response::RemainingBandwidthResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v3::response::PendingRegistrationResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v3::response::RegisteredResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v3::response::RemainingBandwidthResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v3::response::TopUpBandwidthResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v4::response::PendingRegistrationResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v4::response::RegisteredResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v4::response::RemainingBandwidthResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v4::response::TopUpBandwidthResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v5::response::PendingRegistrationResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v5::response::RegisteredResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v5::response::RemainingBandwidthResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v5::response::TopUpBandwidthResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v6::response::PendingRegistrationResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
self.upgrade_mode_enabled.into()
}
}
impl UpgradeModeStatus for v6::response::RegisteredResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
self.upgrade_mode_enabled.into()
}
}
impl UpgradeModeStatus for v6::response::RemainingBandwidthResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
self.upgrade_mode_enabled.into()
}
}
impl UpgradeModeStatus for v6::response::TopUpBandwidthResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
self.upgrade_mode_enabled.into()
}
}
impl UpgradeModeStatus for v6::response::UpgradeModeResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
self.upgrade_mode_enabled.into()
}
}
pub trait InitMessage: Versionable + fmt::Debug {
fn pub_key(&self) -> PeerPublicKey;
@@ -133,14 +297,20 @@ impl InitMessage for v5::registration::InitMessage {
}
}
impl InitMessage for v6::registration::InitMessage {
fn pub_key(&self) -> PeerPublicKey {
self.pub_key
}
}
pub trait FinalMessage: Versionable + fmt::Debug {
fn gateway_client_pub_key(&self) -> PeerPublicKey;
fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error>;
fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error>;
fn private_ips(&self) -> IpPair;
fn gateway_client_ipv4(&self) -> Option<Ipv4Addr>;
fn gateway_client_ipv6(&self) -> Option<Ipv6Addr>;
fn gateway_client_mac(&self) -> Vec<u8>;
fn credential(&self) -> Option<CredentialSpendingData>;
fn credential(&self) -> Option<BandwidthClaim>;
}
impl FinalMessage for v1::GatewayClient {
@@ -148,7 +318,7 @@ impl FinalMessage for v1::GatewayClient {
self.pub_key
}
fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> {
self.verify(private_key, nonce)
}
@@ -171,7 +341,7 @@ impl FinalMessage for v1::GatewayClient {
self.mac.to_vec()
}
fn credential(&self) -> Option<CredentialSpendingData> {
fn credential(&self) -> Option<BandwidthClaim> {
None
}
}
@@ -181,7 +351,7 @@ impl FinalMessage for v2::registration::FinalMessage {
self.gateway_client.pub_key
}
fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> {
self.gateway_client.verify(private_key, nonce)
}
@@ -204,8 +374,12 @@ impl FinalMessage for v2::registration::FinalMessage {
self.gateway_client.mac.to_vec()
}
fn credential(&self) -> Option<CredentialSpendingData> {
self.credential.clone()
fn credential(&self) -> Option<BandwidthClaim> {
self.credential.clone().and_then(|c| {
c.try_into()
.inspect_err(|err| error!("credential conversion error: {err}"))
.ok()
})
}
}
@@ -214,7 +388,7 @@ impl FinalMessage for v3::registration::FinalMessage {
self.gateway_client.pub_key
}
fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> {
self.gateway_client.verify(private_key, nonce)
}
@@ -237,8 +411,12 @@ impl FinalMessage for v3::registration::FinalMessage {
self.gateway_client.mac.to_vec()
}
fn credential(&self) -> Option<CredentialSpendingData> {
self.credential.clone()
fn credential(&self) -> Option<BandwidthClaim> {
self.credential.clone().and_then(|c| {
c.try_into()
.inspect_err(|err| error!("credential conversion error: {err}"))
.ok()
})
}
}
@@ -247,7 +425,42 @@ impl FinalMessage for v4::registration::FinalMessage {
self.gateway_client.pub_key
}
fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> {
self.gateway_client.verify(private_key, nonce)
}
fn private_ips(&self) -> IpPair {
// v4 -> v5 -> v6
v5::registration::IpPair::from(self.gateway_client.private_ips).into()
}
fn gateway_client_ipv4(&self) -> Option<Ipv4Addr> {
Some(self.gateway_client.private_ips.ipv4)
}
fn gateway_client_ipv6(&self) -> Option<Ipv6Addr> {
Some(self.gateway_client.private_ips.ipv6)
}
fn gateway_client_mac(&self) -> Vec<u8> {
self.gateway_client.mac.to_vec()
}
fn credential(&self) -> Option<BandwidthClaim> {
self.credential.clone().and_then(|c| {
c.try_into()
.inspect_err(|err| error!("credential conversion error: {err}"))
.ok()
})
}
}
impl FinalMessage for v5::registration::FinalMessage {
fn gateway_client_pub_key(&self) -> PeerPublicKey {
self.gateway_client.pub_key
}
fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> {
self.gateway_client.verify(private_key, nonce)
}
@@ -267,17 +480,21 @@ impl FinalMessage for v4::registration::FinalMessage {
self.gateway_client.mac.to_vec()
}
fn credential(&self) -> Option<CredentialSpendingData> {
self.credential.clone()
fn credential(&self) -> Option<BandwidthClaim> {
self.credential.clone().and_then(|c| {
c.try_into()
.inspect_err(|err| error!("credential conversion error: {err}"))
.ok()
})
}
}
impl FinalMessage for v5::registration::FinalMessage {
impl FinalMessage for v6::registration::FinalMessage {
fn gateway_client_pub_key(&self) -> PeerPublicKey {
self.gateway_client.pub_key
}
fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> {
self.gateway_client.verify(private_key, nonce)
}
@@ -297,7 +514,7 @@ impl FinalMessage for v5::registration::FinalMessage {
self.gateway_client.mac.to_vec()
}
fn credential(&self) -> Option<CredentialSpendingData> {
fn credential(&self) -> Option<BandwidthClaim> {
self.credential.clone()
}
}
@@ -347,10 +564,42 @@ impl TopUpMessage for v5::topup::TopUpMessage {
}
}
impl TopUpMessage for v6::topup::TopUpMessage {
fn pub_key(&self) -> PeerPublicKey {
self.pub_key
}
fn credential(&self) -> CredentialSpendingData {
self.credential.clone()
}
}
pub trait UpgradeModeMessage: Versionable + fmt::Debug {
// the idea is to expose different types of emergency credentials here,
// like upgrade mode JWT, emergency threshold credential issued by signers, etc.
fn upgrade_mode_global_attestation_jwt(&self) -> Option<String>;
}
impl UpgradeModeMessage for v6::upgrade_mode_check::UpgradeModeCheckRequest {
fn upgrade_mode_global_attestation_jwt(&self) -> Option<String> {
use v6::upgrade_mode_check::UpgradeModeCheckRequest;
match self {
UpgradeModeCheckRequest::UpgradeModeJwt { token } => Some(token.clone()),
}
}
}
pub trait Id {
fn id(&self) -> u64;
}
impl Id for v1::response::PendingRegistrationResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v2::response::PendingRegistrationResponse {
fn id(&self) -> u64 {
self.request_id
@@ -375,6 +624,18 @@ impl Id for v5::response::PendingRegistrationResponse {
}
}
impl Id for v6::response::PendingRegistrationResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v1::response::RegisteredResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v2::response::RegisteredResponse {
fn id(&self) -> u64 {
self.request_id
@@ -399,6 +660,18 @@ impl Id for v5::response::RegisteredResponse {
}
}
impl Id for v6::response::RegisteredResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v1::response::RemainingBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v2::response::RemainingBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
@@ -423,6 +696,12 @@ impl Id for v5::response::RemainingBandwidthResponse {
}
}
impl Id for v6::response::RemainingBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v3::response::TopUpBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
@@ -441,11 +720,28 @@ impl Id for v5::response::TopUpBandwidthResponse {
}
}
pub trait PendingRegistrationResponse: Id + fmt::Debug {
impl Id for v6::response::TopUpBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v6::response::UpgradeModeResponse {
fn id(&self) -> u64 {
self.request_id
}
}
pub trait PendingRegistrationResponse: Id + UpgradeModeStatus + fmt::Debug {
fn nonce(&self) -> u64;
fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error>;
fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error>;
fn pub_key(&self) -> PeerPublicKey;
fn private_ips(&self) -> IpPair;
fn finalise_registration(
&self,
private_key: &x25519::PrivateKey,
credential: Option<BandwidthClaim>,
) -> Box<dyn FinalMessage + Send + Sync>;
}
impl PendingRegistrationResponse for v2::response::PendingRegistrationResponse {
@@ -453,7 +749,7 @@ impl PendingRegistrationResponse for v2::response::PendingRegistrationResponse {
self.reply.nonce
}
fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> {
fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> {
self.reply.gateway_data.verify(gateway_key, self.nonce())
}
@@ -464,6 +760,22 @@ impl PendingRegistrationResponse for v2::response::PendingRegistrationResponse {
fn private_ips(&self) -> IpPair {
self.reply.gateway_data.private_ip.into()
}
fn finalise_registration(
&self,
private_key: &x25519::PrivateKey,
credential: Option<BandwidthClaim>,
) -> Box<dyn FinalMessage + Send + Sync> {
Box::new(v2::registration::FinalMessage {
gateway_client: v2::registration::GatewayClient::new(
private_key,
self.pub_key().inner(),
self.private_ips().ipv4.into(),
self.nonce(),
),
credential: credential.and_then(|b| b.credential.into_zk_nym().map(|c| *c)),
})
}
}
impl PendingRegistrationResponse for v3::response::PendingRegistrationResponse {
@@ -471,7 +783,7 @@ impl PendingRegistrationResponse for v3::response::PendingRegistrationResponse {
self.reply.nonce
}
fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> {
fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> {
self.reply.gateway_data.verify(gateway_key, self.nonce())
}
@@ -482,6 +794,22 @@ impl PendingRegistrationResponse for v3::response::PendingRegistrationResponse {
fn private_ips(&self) -> IpPair {
self.reply.gateway_data.private_ip.into()
}
fn finalise_registration(
&self,
private_key: &x25519::PrivateKey,
credential: Option<BandwidthClaim>,
) -> Box<dyn FinalMessage + Send + Sync> {
Box::new(v3::registration::FinalMessage {
gateway_client: v3::registration::GatewayClient::new(
private_key,
self.pub_key().inner(),
self.private_ips().ipv4.into(),
self.nonce(),
),
credential: credential.and_then(|b| b.credential.into_zk_nym().map(|c| *c)),
})
}
}
impl PendingRegistrationResponse for v4::response::PendingRegistrationResponse {
@@ -489,7 +817,42 @@ impl PendingRegistrationResponse for v4::response::PendingRegistrationResponse {
self.reply.nonce
}
fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> {
fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> {
self.reply.gateway_data.verify(gateway_key, self.nonce())
}
fn pub_key(&self) -> PeerPublicKey {
self.reply.gateway_data.pub_key
}
fn private_ips(&self) -> IpPair {
// v4 -> v5 -> v6
v5::registration::IpPair::from(self.reply.gateway_data.private_ips).into()
}
fn finalise_registration(
&self,
private_key: &x25519::PrivateKey,
credential: Option<BandwidthClaim>,
) -> Box<dyn FinalMessage + Send + Sync> {
Box::new(v4::registration::FinalMessage {
gateway_client: v4::registration::GatewayClient::new(
private_key,
self.pub_key().inner(),
self.reply.gateway_data.private_ips,
self.nonce(),
),
credential: credential.and_then(|b| b.credential.into_zk_nym().map(|c| *c)),
})
}
}
impl PendingRegistrationResponse for v5::response::PendingRegistrationResponse {
fn nonce(&self) -> u64 {
self.reply.nonce
}
fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> {
self.reply.gateway_data.verify(gateway_key, self.nonce())
}
@@ -500,14 +863,30 @@ impl PendingRegistrationResponse for v4::response::PendingRegistrationResponse {
fn private_ips(&self) -> IpPair {
self.reply.gateway_data.private_ips.into()
}
fn finalise_registration(
&self,
private_key: &x25519::PrivateKey,
credential: Option<BandwidthClaim>,
) -> Box<dyn FinalMessage + Send + Sync> {
Box::new(v5::registration::FinalMessage {
gateway_client: v5::registration::GatewayClient::new(
private_key,
self.pub_key().inner(),
self.reply.gateway_data.private_ips,
self.nonce(),
),
credential: credential.and_then(|b| b.credential.into_zk_nym().map(|c| *c)),
})
}
}
impl PendingRegistrationResponse for v5::response::PendingRegistrationResponse {
impl PendingRegistrationResponse for v6::response::PendingRegistrationResponse {
fn nonce(&self) -> u64 {
self.reply.nonce
}
fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> {
fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> {
self.reply.gateway_data.verify(gateway_key, self.nonce())
}
@@ -518,9 +897,25 @@ impl PendingRegistrationResponse for v5::response::PendingRegistrationResponse {
fn private_ips(&self) -> IpPair {
self.reply.gateway_data.private_ips
}
fn finalise_registration(
&self,
private_key: &x25519::PrivateKey,
credential: Option<BandwidthClaim>,
) -> Box<dyn FinalMessage + Send + Sync> {
Box::new(v6::registration::FinalMessage {
gateway_client: v6::registration::GatewayClient::new(
private_key,
self.pub_key().inner(),
self.reply.gateway_data.private_ips,
self.nonce(),
),
credential,
})
}
}
pub trait RegisteredResponse: Id + fmt::Debug {
pub trait RegisteredResponse: Id + UpgradeModeStatus + fmt::Debug {
fn private_ips(&self) -> IpPair;
fn pub_key(&self) -> PeerPublicKey;
fn wg_port(&self) -> u16;
@@ -555,7 +950,8 @@ impl RegisteredResponse for v3::response::RegisteredResponse {
}
impl RegisteredResponse for v4::response::RegisteredResponse {
fn private_ips(&self) -> IpPair {
self.reply.private_ips.into()
// v4 -> v5 -> v6
v5::registration::IpPair::from(self.reply.private_ips).into()
}
fn pub_key(&self) -> PeerPublicKey {
@@ -568,6 +964,20 @@ impl RegisteredResponse for v4::response::RegisteredResponse {
}
impl RegisteredResponse for v5::response::RegisteredResponse {
fn private_ips(&self) -> IpPair {
self.reply.private_ips.into()
}
fn pub_key(&self) -> PeerPublicKey {
self.reply.pub_key
}
fn wg_port(&self) -> u16 {
self.reply.wg_port
}
}
impl RegisteredResponse for v6::response::RegisteredResponse {
fn private_ips(&self) -> IpPair {
self.reply.private_ips
}
@@ -581,7 +991,7 @@ impl RegisteredResponse for v5::response::RegisteredResponse {
}
}
pub trait RemainingBandwidthResponse: Id + fmt::Debug {
pub trait RemainingBandwidthResponse: Id + UpgradeModeStatus + fmt::Debug {
fn available_bandwidth(&self) -> Option<i64>;
}
@@ -609,7 +1019,13 @@ impl RemainingBandwidthResponse for v5::response::RemainingBandwidthResponse {
}
}
pub trait TopUpBandwidthResponse: Id + fmt::Debug {
impl RemainingBandwidthResponse for v6::response::RemainingBandwidthResponse {
fn available_bandwidth(&self) -> Option<i64> {
self.reply.as_ref().map(|r| r.available_bandwidth)
}
}
pub trait TopUpBandwidthResponse: Id + UpgradeModeStatus + fmt::Debug {
fn available_bandwidth(&self) -> i64;
}
@@ -630,3 +1046,9 @@ impl TopUpBandwidthResponse for v5::response::TopUpBandwidthResponse {
self.reply.available_bandwidth
}
}
impl TopUpBandwidthResponse for v6::response::TopUpBandwidthResponse {
fn available_bandwidth(&self) -> i64 {
self.reply.available_bandwidth
}
}
@@ -48,7 +48,7 @@ pub struct RegistrationData {
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RegistredData {
pub struct RegisteredData {
pub pub_key: PeerPublicKey,
pub private_ip: IpAddr,
pub wg_port: u16,
@@ -1,7 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData};
use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData};
use nym_sphinx::addressing::Recipient;
use serde::{Deserialize, Serialize};
@@ -34,7 +34,7 @@ impl AuthenticatorResponse {
}
pub fn new_registered(
registred_data: RegistredData,
registred_data: RegisteredData,
reply_to: Recipient,
request_id: u64,
) -> Self {
@@ -108,7 +108,7 @@ pub struct PendingRegistrationResponse {
pub struct RegisteredResponse {
pub request_id: u64,
pub reply_to: Recipient,
pub reply: RegistredData,
pub reply: RegisteredData,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -154,8 +154,8 @@ impl From<v2::registration::RegistrationData> for v1::registration::Registration
}
}
impl From<v2::registration::RegistredData> for v1::registration::RegistredData {
fn from(value: v2::registration::RegistredData) -> Self {
impl From<v2::registration::RegisteredData> for v1::registration::RegisteredData {
fn from(value: v2::registration::RegisteredData) -> Self {
Self {
pub_key: value.pub_key,
private_ip: value.private_ip,
@@ -58,7 +58,7 @@ pub struct RegistrationData {
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct RegistredData {
pub struct RegisteredData {
pub pub_key: PeerPublicKey,
pub private_ip: IpAddr,
pub wg_port: u16,
@@ -1,7 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData};
use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData};
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use nym_sphinx::addressing::Recipient;
use serde::{Deserialize, Serialize};
@@ -38,7 +38,7 @@ impl AuthenticatorResponse {
}
pub fn new_registered(
registred_data: RegistredData,
registred_data: RegisteredData,
reply_to: Recipient,
request_id: u64,
) -> Self {
@@ -118,7 +118,7 @@ pub struct PendingRegistrationResponse {
pub struct RegisteredResponse {
pub request_id: u64,
pub reply_to: Recipient,
pub reply: RegistredData,
pub reply: RegisteredData,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -299,8 +299,8 @@ impl From<v2::registration::RegistrationData> for v3::registration::Registration
}
}
impl From<v3::registration::RegistredData> for v2::registration::RegistredData {
fn from(value: v3::registration::RegistredData) -> Self {
impl From<v3::registration::RegisteredData> for v2::registration::RegisteredData {
fn from(value: v3::registration::RegisteredData) -> Self {
Self {
pub_key: value.pub_key,
private_ip: value.private_ip,
@@ -309,8 +309,8 @@ impl From<v3::registration::RegistredData> for v2::registration::RegistredData {
}
}
impl From<v2::registration::RegistredData> for v3::registration::RegistredData {
fn from(value: v2::registration::RegistredData) -> Self {
impl From<v2::registration::RegisteredData> for v3::registration::RegisteredData {
fn from(value: v2::registration::RegisteredData) -> Self {
Self {
pub_key: value.pub_key,
private_ip: value.private_ip,
@@ -674,7 +674,7 @@ mod tests {
let pub_key = PeerPublicKey::new(PublicKey::from([0; 32]));
let private_ip = IpAddr::from_str("10.10.10.10").unwrap();
let wg_port = 51822;
let registred_data = v2::registration::RegistredData {
let registred_data = v2::registration::RegisteredData {
pub_key,
private_ip,
wg_port,
@@ -701,7 +701,7 @@ mod tests {
v3::response::AuthenticatorResponseData::Registered(v3::response::RegisteredResponse {
request_id,
reply_to,
reply: v3::registration::RegistredData {
reply: v3::registration::RegisteredData {
wg_port,
pub_key,
private_ip
@@ -715,7 +715,7 @@ mod tests {
let pub_key = PeerPublicKey::new(PublicKey::from([0; 32]));
let private_ip = IpAddr::from_str("10.10.10.10").unwrap();
let wg_port = 51822;
let registred_data = v3::registration::RegistredData {
let registred_data = v3::registration::RegisteredData {
pub_key,
private_ip,
wg_port,
@@ -742,7 +742,7 @@ mod tests {
v2::response::AuthenticatorResponseData::Registered(v2::response::RegisteredResponse {
request_id,
reply_to,
reply: v2::registration::RegistredData {
reply: v2::registration::RegisteredData {
wg_port,
pub_key,
private_ip
@@ -58,7 +58,7 @@ pub struct RegistrationData {
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct RegistredData {
pub struct RegisteredData {
pub pub_key: PeerPublicKey,
pub private_ip: IpAddr,
pub wg_port: u16,
@@ -1,7 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData};
use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData};
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use nym_sphinx::addressing::Recipient;
use serde::{Deserialize, Serialize};
@@ -38,7 +38,7 @@ impl AuthenticatorResponse {
}
pub fn new_registered(
registred_data: RegistredData,
registred_data: RegisteredData,
reply_to: Recipient,
request_id: u64,
) -> Self {
@@ -139,7 +139,7 @@ pub struct PendingRegistrationResponse {
pub struct RegisteredResponse {
pub request_id: u64,
pub reply_to: Recipient,
pub reply: RegistredData,
pub reply: RegisteredData,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -262,8 +262,8 @@ impl From<v4::response::TopUpBandwidthResponse> for v3::response::TopUpBandwidth
}
}
impl From<v3::registration::RegistredData> for v4::registration::RegistredData {
fn from(value: v3::registration::RegistredData) -> Self {
impl From<v3::registration::RegisteredData> for v4::registration::RegisteredData {
fn from(value: v3::registration::RegisteredData) -> Self {
Self {
pub_key: value.pub_key,
private_ips: value.private_ip.into(),
@@ -272,8 +272,8 @@ impl From<v3::registration::RegistredData> for v4::registration::RegistredData {
}
}
impl From<v4::registration::RegistredData> for v3::registration::RegistredData {
fn from(value: v4::registration::RegistredData) -> Self {
impl From<v4::registration::RegisteredData> for v3::registration::RegisteredData {
fn from(value: v4::registration::RegisteredData) -> Self {
Self {
pub_key: value.pub_key,
private_ip: value.private_ips.ipv4.into(),
@@ -565,7 +565,7 @@ mod tests {
let private_ips =
v4::registration::IpPair::new(ipv4, Ipv6Addr::from_str("fc01::a0a").unwrap());
let wg_port = 51822;
let registred_data = v3::registration::RegistredData {
let registred_data = v3::registration::RegisteredData {
pub_key,
private_ip: ipv4.into(),
wg_port,
@@ -592,7 +592,7 @@ mod tests {
v4::response::AuthenticatorResponseData::Registered(v4::response::RegisteredResponse {
request_id,
reply_to,
reply: v4::registration::RegistredData {
reply: v4::registration::RegisteredData {
wg_port,
pub_key,
private_ips
@@ -608,7 +608,7 @@ mod tests {
let private_ips =
v4::registration::IpPair::new(ipv4, Ipv6Addr::from_str("fc01::10").unwrap());
let wg_port = 51822;
let registred_data = v4::registration::RegistredData {
let registred_data = v4::registration::RegisteredData {
pub_key,
private_ips,
wg_port,
@@ -635,7 +635,7 @@ mod tests {
v3::response::AuthenticatorResponseData::Registered(v3::response::RegisteredResponse {
request_id,
reply_to,
reply: v3::registration::RegistredData {
reply: v3::registration::RegisteredData {
wg_port,
pub_key,
private_ip: ipv4.into()
@@ -110,7 +110,7 @@ pub struct RegistrationData {
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct RegistredData {
pub struct RegisteredData {
pub pub_key: PeerPublicKey,
pub private_ips: IpPair,
pub wg_port: u16,
@@ -1,7 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData};
use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData};
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use nym_sphinx::addressing::Recipient;
use serde::{Deserialize, Serialize};
@@ -38,7 +38,7 @@ impl AuthenticatorResponse {
}
pub fn new_registered(
registred_data: RegistredData,
registred_data: RegisteredData,
reply_to: Recipient,
request_id: u64,
) -> Self {
@@ -139,7 +139,7 @@ pub struct PendingRegistrationResponse {
pub struct RegisteredResponse {
pub request_id: u64,
pub reply_to: Recipient,
pub reply: RegistredData,
pub reply: RegisteredData,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -186,8 +186,8 @@ impl From<v4::response::TopUpBandwidthResponse> for v5::response::TopUpBandwidth
}
}
impl From<v4::registration::RegistredData> for v5::registration::RegistredData {
fn from(value: v4::registration::RegistredData) -> Self {
impl From<v4::registration::RegisteredData> for v5::registration::RegisteredData {
fn from(value: v4::registration::RegisteredData) -> Self {
Self {
pub_key: value.pub_key,
private_ips: value.private_ips.into(),
@@ -405,7 +405,7 @@ mod tests {
let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap();
let private_ips = v4::registration::IpPair::new(ipv4, ipv6);
let wg_port = 51822;
let registred_data = v4::registration::RegistredData {
let registred_data = v4::registration::RegisteredData {
pub_key,
private_ips,
wg_port,
@@ -431,7 +431,7 @@ mod tests {
upgraded_msg.data,
v5::response::AuthenticatorResponseData::Registered(v5::response::RegisteredResponse {
request_id,
reply: v5::registration::RegistredData {
reply: v5::registration::RegisteredData {
wg_port,
pub_key,
private_ips: v5::registration::IpPair::new(ipv4, ipv6)
@@ -108,7 +108,7 @@ pub struct RegistrationData {
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct RegistredData {
pub struct RegisteredData {
pub pub_key: PeerPublicKey,
pub private_ips: IpPair,
pub wg_port: u16,
@@ -1,7 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData};
use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData};
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use serde::{Deserialize, Serialize};
@@ -32,7 +32,7 @@ impl AuthenticatorResponse {
}
}
pub fn new_registered(registred_data: RegistredData, request_id: u64) -> Self {
pub fn new_registered(registred_data: RegisteredData, request_id: u64) -> Self {
Self {
protocol: Protocol {
service_provider_type: ServiceProviderType::Authenticator,
@@ -116,7 +116,7 @@ pub struct PendingRegistrationResponse {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct RegisteredResponse {
pub request_id: u64,
pub reply: RegistredData,
pub reply: RegisteredData,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -0,0 +1,441 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{v5, v6};
impl TryFrom<v5::request::AuthenticatorRequest> for v6::request::AuthenticatorRequest {
type Error = crate::Error;
fn try_from(
authenticator_request: v5::request::AuthenticatorRequest,
) -> Result<Self, Self::Error> {
Ok(Self {
protocol: v6::PROTOCOL,
data: authenticator_request.data.try_into()?,
request_id: authenticator_request.request_id,
})
}
}
impl TryFrom<v5::request::AuthenticatorRequestData> for v6::request::AuthenticatorRequestData {
type Error = crate::Error;
fn try_from(
authenticator_request_data: v5::request::AuthenticatorRequestData,
) -> Result<Self, Self::Error> {
match authenticator_request_data {
v5::request::AuthenticatorRequestData::Initial(init_msg) => Ok(
v6::request::AuthenticatorRequestData::Initial(init_msg.into()),
),
v5::request::AuthenticatorRequestData::Final(final_msg) => Ok(
v6::request::AuthenticatorRequestData::Final(Box::new((*final_msg).try_into()?)),
),
v5::request::AuthenticatorRequestData::QueryBandwidth(pub_key) => Ok(
v6::request::AuthenticatorRequestData::QueryBandwidth(pub_key),
),
v5::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => Ok(
v6::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message.into()),
),
}
}
}
impl From<v5::registration::InitMessage> for v6::registration::InitMessage {
fn from(init_msg: v5::registration::InitMessage) -> Self {
Self {
pub_key: init_msg.pub_key,
}
}
}
impl TryFrom<v5::registration::FinalMessage> for v6::registration::FinalMessage {
type Error = crate::Error;
fn try_from(final_msg: v5::registration::FinalMessage) -> Result<Self, Self::Error> {
Ok(Self {
gateway_client: final_msg.gateway_client.into(),
credential: final_msg
.credential
.map(TryInto::try_into)
.transpose()
.map_err(Self::Error::conversion_display)?,
})
}
}
impl From<v5::registration::GatewayClient> for v6::registration::GatewayClient {
fn from(gateway_client: v5::registration::GatewayClient) -> Self {
Self {
pub_key: gateway_client.pub_key,
private_ips: gateway_client.private_ips.into(),
mac: gateway_client.mac.into(),
}
}
}
impl From<v6::registration::GatewayClient> for v5::registration::GatewayClient {
fn from(gateway_client: v6::registration::GatewayClient) -> Self {
Self {
pub_key: gateway_client.pub_key,
private_ips: gateway_client.private_ips.into(),
mac: gateway_client.mac.into(),
}
}
}
impl From<v5::registration::ClientMac> for v6::registration::ClientMac {
fn from(client_mac: v5::registration::ClientMac) -> Self {
Self::new((*client_mac).clone())
}
}
impl From<v6::registration::ClientMac> for v5::registration::ClientMac {
fn from(client_mac: v6::registration::ClientMac) -> Self {
Self::new((*client_mac).clone())
}
}
impl From<Box<v5::topup::TopUpMessage>> for Box<v6::topup::TopUpMessage> {
fn from(top_up_message: Box<v5::topup::TopUpMessage>) -> Self {
Box::new(v6::topup::TopUpMessage {
pub_key: top_up_message.pub_key,
credential: top_up_message.credential,
})
}
}
impl From<v5::response::AuthenticatorResponse> for v6::response::AuthenticatorResponse {
fn from(value: v5::response::AuthenticatorResponse) -> Self {
Self {
protocol: v6::PROTOCOL,
data: value.data.into(),
}
}
}
impl From<v5::response::AuthenticatorResponseData> for v6::response::AuthenticatorResponseData {
fn from(authenticator_response_data: v5::response::AuthenticatorResponseData) -> Self {
match authenticator_response_data {
v5::response::AuthenticatorResponseData::PendingRegistration(pending_response) => {
v6::response::AuthenticatorResponseData::PendingRegistration(
pending_response.into(),
)
}
v5::response::AuthenticatorResponseData::Registered(registered_response) => {
v6::response::AuthenticatorResponseData::Registered(registered_response.into())
}
v5::response::AuthenticatorResponseData::RemainingBandwidth(
remaining_bandwidth_response,
) => v6::response::AuthenticatorResponseData::RemainingBandwidth(
remaining_bandwidth_response.into(),
),
v5::response::AuthenticatorResponseData::TopUpBandwidth(top_up_response) => {
v6::response::AuthenticatorResponseData::TopUpBandwidth(top_up_response.into())
}
}
}
}
impl From<v5::response::RegisteredResponse> for v6::response::RegisteredResponse {
fn from(value: v5::response::RegisteredResponse) -> Self {
Self {
request_id: value.request_id,
reply: value.reply.into(),
upgrade_mode_enabled: false,
}
}
}
impl From<v5::response::PendingRegistrationResponse> for v6::response::PendingRegistrationResponse {
fn from(value: v5::response::PendingRegistrationResponse) -> Self {
Self {
request_id: value.request_id,
reply: value.reply.into(),
upgrade_mode_enabled: false,
}
}
}
impl From<v5::registration::RegistrationData> for v6::registration::RegistrationData {
fn from(value: v5::registration::RegistrationData) -> Self {
Self {
nonce: value.nonce,
gateway_data: value.gateway_data.into(),
wg_port: value.wg_port,
}
}
}
impl From<v6::registration::RegistrationData> for v5::registration::RegistrationData {
fn from(value: v6::registration::RegistrationData) -> Self {
Self {
nonce: value.nonce,
gateway_data: value.gateway_data.into(),
wg_port: value.wg_port,
}
}
}
impl From<v5::response::RemainingBandwidthResponse> for v6::response::RemainingBandwidthResponse {
fn from(value: v5::response::RemainingBandwidthResponse) -> Self {
Self {
request_id: value.request_id,
reply: value.reply.map(Into::into),
upgrade_mode_enabled: false,
}
}
}
impl From<v5::response::TopUpBandwidthResponse> for v6::response::TopUpBandwidthResponse {
fn from(value: v5::response::TopUpBandwidthResponse) -> Self {
Self {
request_id: value.request_id,
reply: value.reply.into(),
upgrade_mode_enabled: false,
}
}
}
impl From<v5::registration::RegisteredData> for v6::registration::RegisteredData {
fn from(value: v5::registration::RegisteredData) -> Self {
Self {
pub_key: value.pub_key,
private_ips: value.private_ips.into(),
wg_port: value.wg_port,
}
}
}
impl From<v5::registration::RemainingBandwidthData> for v6::registration::RemainingBandwidthData {
fn from(value: v5::registration::RemainingBandwidthData) -> Self {
Self {
available_bandwidth: value.available_bandwidth,
}
}
}
impl From<v5::registration::IpPair> for v6::registration::IpPair {
fn from(value: v5::registration::IpPair) -> Self {
Self {
ipv4: value.ipv4,
ipv6: value.ipv6,
}
}
}
impl From<v6::registration::IpPair> for v5::registration::IpPair {
fn from(value: v6::registration::IpPair) -> Self {
Self {
ipv4: value.ipv4,
ipv6: value.ipv6,
}
}
}
#[cfg(test)]
mod tests {
use std::{
net::{Ipv4Addr, Ipv6Addr},
str::FromStr,
};
use nym_credentials_interface::{BandwidthCredential, CredentialSpendingData, TicketType};
use nym_crypto::asymmetric::x25519::PrivateKey;
use nym_wireguard_types::PeerPublicKey;
use x25519_dalek::PublicKey;
use super::*;
use crate::models::BandwidthClaim;
use crate::{util::tests::CREDENTIAL_BYTES, v5};
#[test]
fn upgrade_initial_req() {
let pub_key = PeerPublicKey::new(PublicKey::from([0; 32]));
let (msg, _) = v5::request::AuthenticatorRequest::new_initial_request(
v5::registration::InitMessage::new(pub_key),
);
let upgraded_msg = v6::request::AuthenticatorRequest::try_from(msg).unwrap();
assert_eq!(upgraded_msg.protocol, v6::PROTOCOL);
assert_eq!(
upgraded_msg.data,
v6::request::AuthenticatorRequestData::Initial(v6::registration::InitMessage {
pub_key
})
);
}
#[test]
fn upgrade_final_req() {
let mut rng = rand::thread_rng();
let local_secret = PrivateKey::new(&mut rng);
let remote_secret = x25519_dalek::StaticSecret::random_from_rng(&mut rng);
let ipv4 = Ipv4Addr::from_str("10.10.10.10").unwrap();
let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap();
let ips = v5::registration::IpPair::new(ipv4, ipv6);
let nonce = 42;
let gateway_client = v5::registration::GatewayClient::new(
&local_secret,
(&remote_secret).into(),
ips,
nonce,
);
let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap();
let final_message = v5::registration::FinalMessage {
gateway_client: gateway_client.clone(),
credential: Some(credential.clone()),
};
let (msg, _) = v5::request::AuthenticatorRequest::new_final_request(final_message);
let upgraded_msg = v6::request::AuthenticatorRequest::try_from(msg).unwrap();
assert_eq!(upgraded_msg.protocol, v6::PROTOCOL);
assert_eq!(
upgraded_msg.data,
v6::request::AuthenticatorRequestData::Final(Box::new(
v6::registration::FinalMessage {
gateway_client: v6::registration::GatewayClient::new(
&local_secret,
(&remote_secret).into(),
v6::registration::IpPair::new(ipv4, ipv6),
nonce
),
credential: Some(BandwidthClaim {
credential: BandwidthCredential::ZkNym(Box::new(credential)),
kind: TicketType::V1MixnetEntry,
})
}
))
);
}
#[test]
fn upgrade_query_req() {
let pub_key = PeerPublicKey::new(PublicKey::from([0; 32]));
let (msg, _) = v5::request::AuthenticatorRequest::new_query_request(pub_key);
let upgraded_msg = v6::request::AuthenticatorRequest::try_from(msg).unwrap();
assert_eq!(upgraded_msg.protocol, v6::PROTOCOL);
assert_eq!(
upgraded_msg.data,
v6::request::AuthenticatorRequestData::QueryBandwidth(pub_key)
);
}
#[test]
fn upgrade_pending_reg_resp() {
let mut rng = rand::thread_rng();
let local_secret = PrivateKey::new(&mut rng);
let remote_secret = x25519_dalek::StaticSecret::random_from_rng(&mut rng);
let ipv4 = Ipv4Addr::from_str("10.10.10.10").unwrap();
let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap();
let ips = v5::registration::IpPair::new(ipv4, ipv6);
let nonce = 42;
let wg_port = 51822;
let gateway_data = v5::registration::GatewayClient::new(
&local_secret,
(&remote_secret).into(),
ips,
nonce,
);
let registration_data = v5::registration::RegistrationData {
nonce,
gateway_data,
wg_port,
};
let request_id = 123;
let msg = v5::response::AuthenticatorResponse::new_pending_registration_success(
registration_data,
request_id,
);
let upgraded_msg = v6::response::AuthenticatorResponse::from(msg);
assert_eq!(upgraded_msg.protocol, v6::PROTOCOL);
assert_eq!(
upgraded_msg.data,
v6::response::AuthenticatorResponseData::PendingRegistration(
v6::response::PendingRegistrationResponse {
request_id,
reply: v6::registration::RegistrationData {
nonce,
gateway_data: v6::registration::GatewayClient::new(
&local_secret,
(&remote_secret).into(),
v6::registration::IpPair::new(ipv4, ipv6),
nonce
),
wg_port
},
upgrade_mode_enabled: false,
}
)
);
}
#[test]
fn upgrade_registered_resp() {
let pub_key = PeerPublicKey::new(PublicKey::from([0; 32]));
let ipv4 = Ipv4Addr::from_str("10.1.10.10").unwrap();
let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap();
let private_ips = v5::registration::IpPair::new(ipv4, ipv6);
let wg_port = 51822;
let registered_data = v5::registration::RegisteredData {
pub_key,
private_ips,
wg_port,
};
let request_id = 123;
let msg = v5::response::AuthenticatorResponse::new_registered(registered_data, request_id);
let upgraded_msg = v6::response::AuthenticatorResponse::from(msg);
assert_eq!(upgraded_msg.protocol, v6::PROTOCOL);
assert_eq!(
upgraded_msg.data,
v6::response::AuthenticatorResponseData::Registered(v6::response::RegisteredResponse {
request_id,
reply: v6::registration::RegisteredData {
wg_port,
pub_key,
private_ips: v6::registration::IpPair::new(ipv4, ipv6)
},
upgrade_mode_enabled: false,
})
);
}
#[test]
fn upgrade_remaining_bandwidth_resp() {
let available_bandwidth = 42;
let remaining_bandwidth_data = Some(v5::registration::RemainingBandwidthData {
available_bandwidth,
});
let request_id = 123;
let msg = v5::response::AuthenticatorResponse::new_remaining_bandwidth(
remaining_bandwidth_data,
request_id,
);
let upgraded_msg = v6::response::AuthenticatorResponse::from(msg);
assert_eq!(upgraded_msg.protocol, v6::PROTOCOL);
assert_eq!(
upgraded_msg.data,
v6::response::AuthenticatorResponseData::RemainingBandwidth(
v6::response::RemainingBandwidthResponse {
request_id,
reply: Some(v6::registration::RemainingBandwidthData {
available_bandwidth,
}),
upgrade_mode_enabled: false,
}
)
);
}
}
@@ -0,0 +1,15 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
pub mod conversion;
pub mod registration;
pub mod request;
pub mod response;
pub mod topup;
pub mod upgrade_mode_check;
pub const VERSION: u8 = 6;
pub const PROTOCOL: Protocol = Protocol::new(VERSION, ServiceProviderType::Authenticator);
@@ -0,0 +1,287 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::Error;
use crate::models::BandwidthClaim;
use base64::{Engine, engine::general_purpose};
use nym_network_defaults::constants::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6};
use nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::time::SystemTime;
use std::{fmt, ops::Deref, str::FromStr};
#[cfg(feature = "verify")]
use hmac::{Hmac, Mac};
#[cfg(feature = "verify")]
use nym_crypto::asymmetric::x25519::{PrivateKey, PublicKey};
#[cfg(feature = "verify")]
use sha2::Sha256;
pub type PendingRegistrations = HashMap<PeerPublicKey, RegistrationData>;
pub type PrivateIPs = HashMap<IpPair, Taken>;
#[cfg(feature = "verify")]
pub type HmacSha256 = Hmac<Sha256>;
pub type Nonce = u64;
pub type Taken = Option<SystemTime>;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct IpPair {
pub ipv4: Ipv4Addr,
pub ipv6: Ipv6Addr,
}
impl IpPair {
pub fn new(ipv4: Ipv4Addr, ipv6: Ipv6Addr) -> Self {
IpPair { ipv4, ipv6 }
}
}
impl From<(Ipv4Addr, Ipv6Addr)> for IpPair {
fn from((ipv4, ipv6): (Ipv4Addr, Ipv6Addr)) -> Self {
IpPair { ipv4, ipv6 }
}
}
impl fmt::Display for IpPair {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.ipv4, self.ipv6)
}
}
impl From<IpAddr> for IpPair {
fn from(value: IpAddr) -> Self {
let (before_last_byte, last_byte) = match value {
IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]),
IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]),
};
let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16;
let ipv4 = Ipv4Addr::new(
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0],
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1],
before_last_byte,
last_byte,
);
let ipv6 = Ipv6Addr::new(
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6],
last_bytes,
);
IpPair::new(ipv4, ipv6)
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct InitMessage {
/// Base64 encoded x25519 public key
pub pub_key: PeerPublicKey,
}
impl InitMessage {
pub fn new(pub_key: PeerPublicKey) -> Self {
InitMessage { pub_key }
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct FinalMessage {
/// Gateway client data
pub gateway_client: GatewayClient,
/// Ecash credential
pub credential: Option<BandwidthClaim>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct RegistrationData {
pub nonce: u64,
pub gateway_data: GatewayClient,
pub wg_port: u16,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct RegisteredData {
pub pub_key: PeerPublicKey,
pub private_ips: IpPair,
pub wg_port: u16,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct RemainingBandwidthData {
pub available_bandwidth: i64,
}
/// Client that wants to register sends its PublicKey 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, PartialEq)]
pub struct GatewayClient {
/// Base64 encoded x25519 public key
pub pub_key: PeerPublicKey,
/// Assigned private IPs (v4 and v6)
pub private_ips: IpPair,
/// Sha256 hmac on the data (alongside the prior nonce)
pub mac: ClientMac,
}
impl GatewayClient {
#[cfg(feature = "verify")]
pub fn new(
local_secret: &PrivateKey,
remote_public: x25519_dalek::PublicKey,
private_ips: IpPair,
nonce: u64,
) -> Self {
let local_public = PublicKey::from(local_secret);
let remote_public = PublicKey::from(remote_public);
let dh = local_secret.diffie_hellman(&remote_public);
// TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)]
let mut mac = HmacSha256::new_from_slice(&dh[..])
.expect("x25519 shared secret is always 32 bytes long");
mac.update(local_public.as_bytes());
mac.update(private_ips.to_string().as_bytes());
mac.update(&nonce.to_le_bytes());
GatewayClient {
pub_key: PeerPublicKey::new(local_public.into()),
private_ips,
mac: ClientMac(mac.finalize().into_bytes().to_vec()),
}
}
// Reusable secret should be gateways Wireguard PK
// Client should perform this step when generating its payload, using its own WG PK
#[cfg(feature = "verify")]
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
// use gateways key as a ref to an x25519_dalek key
let dh = gateway_key.inner().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.private_ips.to_string().as_bytes());
mac.update(&nonce.to_le_bytes());
mac.verify_slice(&self.mac)
.map_err(|source| Error::FailedClientMacVerification {
client: self.pub_key.to_string(),
source,
})
}
pub fn pub_key(&self) -> PeerPublicKey {
self.pub_key
}
}
// TODO: change the inner type into generic array of size HmacSha256::OutputSize
// TODO2: rely on our internal crypto/hmac
#[derive(Debug, Clone, PartialEq)]
pub struct ClientMac(Vec<u8>);
impl fmt::Display for ClientMac {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", general_purpose::STANDARD.encode(&self.0))
}
}
impl From<Vec<u8>> for ClientMac {
fn from(v: Vec<u8>) -> Self {
ClientMac(v)
}
}
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 FromStr for ClientMac {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mac_bytes: Vec<u8> =
general_purpose::STANDARD
.decode(s)
.map_err(|source| Error::MalformedClientMac {
mac: s.to_string(),
source,
})?;
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)
}
}
#[cfg(test)]
mod tests {
use super::*;
use nym_crypto::asymmetric::x25519;
use nym_test_utils::helpers::deterministic_rng;
#[test]
fn create_ip_pair() {
let ipv4: IpAddr = Ipv4Addr::from_str("10.1.10.50").unwrap().into();
let ipv6: IpAddr = Ipv6Addr::from_str("fc01::0a32").unwrap().into();
assert_eq!(IpPair::from(ipv4), IpPair::from(ipv6));
}
#[test]
#[cfg(feature = "verify")]
fn client_request_roundtrip() {
let mut rng = deterministic_rng();
let gateway_key_pair = x25519::KeyPair::new(&mut rng);
let client_key_pair = x25519::KeyPair::new(&mut rng);
let nonce = 1234567890;
let client = GatewayClient::new(
client_key_pair.private_key(),
x25519_dalek::PublicKey::from(gateway_key_pair.public_key().to_bytes()),
IpPair::new("10.0.0.42".parse().unwrap(), "fc00::42".parse().unwrap()),
nonce,
);
assert!(client.verify(gateway_key_pair.private_key(), nonce).is_ok())
}
}
@@ -0,0 +1,135 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::{
PROTOCOL,
registration::{FinalMessage, InitMessage},
topup::TopUpMessage,
upgrade_mode_check::UpgradeModeCheckRequest,
};
use nym_service_provider_requests_common::Protocol;
use nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize};
use crate::make_bincode_serializer;
fn generate_random() -> u64 {
use rand::RngCore;
let mut rng = rand::rngs::OsRng;
rng.next_u64()
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct AuthenticatorRequest {
pub protocol: Protocol,
pub data: AuthenticatorRequestData,
pub request_id: u64,
}
impl AuthenticatorRequest {
pub fn from_reconstructed_message(
message: &nym_sphinx::receiver::ReconstructedMessage,
) -> Result<Self, bincode::Error> {
use bincode::Options;
make_bincode_serializer().deserialize(&message.message)
}
pub fn new_initial_request(init_message: InitMessage) -> (Self, u64) {
let request_id = generate_random();
(
Self {
protocol: PROTOCOL,
data: AuthenticatorRequestData::Initial(init_message),
request_id,
},
request_id,
)
}
pub fn new_final_request(final_message: FinalMessage) -> (Self, u64) {
let request_id = generate_random();
(
Self {
protocol: PROTOCOL,
data: AuthenticatorRequestData::Final(Box::new(final_message)),
request_id,
},
request_id,
)
}
pub fn new_query_request(peer_public_key: PeerPublicKey) -> (Self, u64) {
let request_id = generate_random();
(
Self {
protocol: PROTOCOL,
data: AuthenticatorRequestData::QueryBandwidth(peer_public_key),
request_id,
},
request_id,
)
}
pub fn new_topup_request(top_up_message: TopUpMessage) -> (Self, u64) {
let request_id = generate_random();
(
Self {
protocol: PROTOCOL,
data: AuthenticatorRequestData::TopUpBandwidth(Box::new(top_up_message)),
request_id,
},
request_id,
)
}
pub fn new_upgrade_mode_check_request(message: UpgradeModeCheckRequest) -> (Self, u64) {
let request_id = generate_random();
(
Self {
protocol: PROTOCOL,
data: AuthenticatorRequestData::CheckUpgradeMode(message),
request_id,
},
request_id,
)
}
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
make_bincode_serializer().serialize(self)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum AuthenticatorRequestData {
Initial(InitMessage),
Final(Box<FinalMessage>),
QueryBandwidth(PeerPublicKey),
TopUpBandwidth(Box<TopUpMessage>),
CheckUpgradeMode(UpgradeModeCheckRequest),
}
#[cfg(test)]
mod tests {
use super::super::VERSION;
use super::*;
use nym_service_provider_requests_common::ServiceProviderType;
use std::str::FromStr;
#[test]
fn check_first_bytes_protocol() {
let version = VERSION;
let data = AuthenticatorRequest {
protocol: Protocol {
version,
service_provider_type: ServiceProviderType::Authenticator,
},
data: AuthenticatorRequestData::Initial(InitMessage::new(
PeerPublicKey::from_str("yvNUDpT5l7W/xDhiu6HkqTHDQwbs/B3J5UrLmORl1EQ=").unwrap(),
)),
request_id: 1,
};
let bytes = *data.to_bytes().unwrap().first_chunk::<2>().unwrap();
assert_eq!(bytes, [version, ServiceProviderType::Authenticator as u8]);
}
}
@@ -0,0 +1,153 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData};
use nym_service_provider_requests_common::Protocol;
use serde::{Deserialize, Serialize};
use crate::make_bincode_serializer;
use super::PROTOCOL;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct AuthenticatorResponse {
pub protocol: Protocol,
pub data: AuthenticatorResponseData,
}
impl AuthenticatorResponse {
pub fn new_pending_registration_success(
registration_data: RegistrationData,
request_id: u64,
upgrade_mode_enabled: bool,
) -> Self {
Self {
protocol: PROTOCOL,
data: AuthenticatorResponseData::PendingRegistration(PendingRegistrationResponse {
reply: registration_data,
request_id,
upgrade_mode_enabled,
}),
}
}
pub fn new_registered(
registered_data: RegisteredData,
request_id: u64,
upgrade_mode_enabled: bool,
) -> Self {
Self {
protocol: PROTOCOL,
data: AuthenticatorResponseData::Registered(RegisteredResponse {
reply: registered_data,
request_id,
upgrade_mode_enabled,
}),
}
}
pub fn new_remaining_bandwidth(
remaining_bandwidth_data: Option<RemainingBandwidthData>,
request_id: u64,
upgrade_mode_enabled: bool,
) -> Self {
Self {
protocol: PROTOCOL,
data: AuthenticatorResponseData::RemainingBandwidth(RemainingBandwidthResponse {
reply: remaining_bandwidth_data,
request_id,
upgrade_mode_enabled,
}),
}
}
pub fn new_topup_bandwidth(
remaining_bandwidth_data: RemainingBandwidthData,
request_id: u64,
upgrade_mode_enabled: bool,
) -> Self {
Self {
protocol: PROTOCOL,
data: AuthenticatorResponseData::TopUpBandwidth(TopUpBandwidthResponse {
reply: remaining_bandwidth_data,
request_id,
upgrade_mode_enabled,
}),
}
}
pub fn new_upgrade_mode_check(request_id: u64, upgrade_mode_enabled: bool) -> Self {
Self {
protocol: PROTOCOL,
data: AuthenticatorResponseData::UpgradeMode(UpgradeModeResponse {
request_id,
upgrade_mode_enabled,
}),
}
}
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
make_bincode_serializer().serialize(self)
}
pub fn from_reconstructed_message(
message: &nym_sphinx::receiver::ReconstructedMessage,
) -> Result<Self, bincode::Error> {
use bincode::Options;
make_bincode_serializer().deserialize(&message.message)
}
pub fn id(&self) -> Option<u64> {
match &self.data {
AuthenticatorResponseData::PendingRegistration(response) => Some(response.request_id),
AuthenticatorResponseData::Registered(response) => Some(response.request_id),
AuthenticatorResponseData::RemainingBandwidth(response) => Some(response.request_id),
AuthenticatorResponseData::TopUpBandwidth(response) => Some(response.request_id),
AuthenticatorResponseData::UpgradeMode(response) => Some(response.request_id),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum AuthenticatorResponseData {
PendingRegistration(PendingRegistrationResponse),
Registered(RegisteredResponse),
RemainingBandwidth(RemainingBandwidthResponse),
TopUpBandwidth(TopUpBandwidthResponse),
UpgradeMode(UpgradeModeResponse),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct PendingRegistrationResponse {
pub request_id: u64,
pub reply: RegistrationData,
pub upgrade_mode_enabled: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct RegisteredResponse {
pub request_id: u64,
pub reply: RegisteredData,
pub upgrade_mode_enabled: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct RemainingBandwidthResponse {
pub request_id: u64,
pub reply: Option<RemainingBandwidthData>,
pub upgrade_mode_enabled: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct TopUpBandwidthResponse {
pub request_id: u64,
pub reply: RemainingBandwidthData,
pub upgrade_mode_enabled: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct UpgradeModeResponse {
pub request_id: u64,
pub upgrade_mode_enabled: bool,
}
@@ -0,0 +1,15 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_credentials_interface::CredentialSpendingData;
use nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct TopUpMessage {
/// Base64 encoded x25519 public key
pub pub_key: PeerPublicKey,
/// Ecash credential
pub credential: CredentialSpendingData,
}
@@ -0,0 +1,12 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum UpgradeModeCheckRequest {
/// Attempt to request upgrade mode recheck via the JWT issued as the result of
/// global attestation.json being published
UpgradeModeJwt { token: String },
}
+21 -5
View File
@@ -1,7 +1,7 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::{v1, v2, v3, v4, v5};
use super::{v1, v2, v3, v4, v5, v6};
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
#[derive(Copy, Clone, Debug, PartialEq, strum_macros::Display)]
@@ -22,11 +22,15 @@ pub enum AuthenticatorVersion {
/// introduced in dorina-patched release (1.6.1)
V5,
/// introduced in niolo release (1.23.0)
V6,
/// an unknown, future, variant that can be present if running outdated software
UNKNOWN,
}
impl AuthenticatorVersion {
pub const LATEST: Self = Self::V5;
pub const LATEST: Self = Self::V6;
pub const fn release_version(&self) -> semver::Version {
match self {
@@ -35,6 +39,7 @@ impl AuthenticatorVersion {
AuthenticatorVersion::V3 => semver::Version::new(1, 1, 10),
AuthenticatorVersion::V4 => semver::Version::new(1, 2, 0),
AuthenticatorVersion::V5 => semver::Version::new(1, 6, 1),
AuthenticatorVersion::V6 => semver::Version::new(1, 23, 0),
AuthenticatorVersion::UNKNOWN => semver::Version::new(0, 0, 0),
}
}
@@ -54,6 +59,8 @@ impl From<Protocol> for AuthenticatorVersion {
AuthenticatorVersion::V4
} else if value.version == v5::VERSION {
AuthenticatorVersion::V5
} else if value.version == v6::VERSION {
AuthenticatorVersion::V6
} else {
AuthenticatorVersion::UNKNOWN
}
@@ -72,6 +79,8 @@ impl From<u8> for AuthenticatorVersion {
AuthenticatorVersion::V4
} else if value == v5::VERSION {
AuthenticatorVersion::V5
} else if value == v6::VERSION {
AuthenticatorVersion::V6
} else {
AuthenticatorVersion::UNKNOWN
}
@@ -126,11 +135,14 @@ impl From<semver::Version> for AuthenticatorVersion {
if semver < AuthenticatorVersion::V5.release_version() {
return Self::V4;
}
// if provided version is higher (or equal) to release version of V5,
// we return the latest (i.e. v5)
if semver < AuthenticatorVersion::V6.release_version() {
return Self::V5;
}
// if provided version is higher (or equal) to release version of V6,
// we return the latest (i.e. v6)
debug_assert_eq!(
Self::V5,
Self::V6,
Self::LATEST,
"a new AuthenticatorVersion variant has been introduced without adjusting the `From<semver::Version>` trait"
);
@@ -191,5 +203,9 @@ mod tests {
assert_eq!(AuthenticatorVersion::V5, "1.7.0".into());
assert_eq!(AuthenticatorVersion::V5, "1.16.11".into());
assert_eq!(AuthenticatorVersion::V5, "1.17.0".into());
assert_eq!(AuthenticatorVersion::V5, "1.22.0".into());
assert_eq!(AuthenticatorVersion::V6, "1.23.0".into());
assert_eq!(AuthenticatorVersion::V6, "1.23.1".into());
assert_eq!(AuthenticatorVersion::V6, "1.24.0".into());
}
}
+1 -6
View File
@@ -7,21 +7,16 @@ license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-trait = { workspace = true }
bip39 = { workspace = true }
async-trait = { workspace = true }
log = { workspace = true }
rand = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true }
zeroize = { workspace = true }
nym-credential-storage = { path = "../credential-storage" }
nym-credentials = { path = "../credentials" }
nym-credentials-interface = { path = "../credentials-interface" }
nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "stream_cipher", "aes", "hashing"] }
nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" }
nym-ecash-time = { path = "../ecash-time" }
nym-network-defaults = { path = "../network-defaults" }
nym-task = { path = "../task" }
nym-validator-client = { path = "../client-libs/validator-client", default-features = false }
+3
View File
@@ -21,6 +21,9 @@ pub enum BandwidthControllerError {
#[error("There was a credential storage error - {0}")]
CredentialStorageError(Box<dyn std::error::Error + Send + Sync>),
#[error("retrieved upgrade mode token is not a valid String")]
MalformedUpgradeModeToken,
#[error("the credential storage does not contain any usable credentials")]
NoCredentialsAvailable,
+14 -1
View File
@@ -12,7 +12,7 @@ use crate::utils::{
ApiClientsWrapper,
};
use log::error;
use nym_credential_storage::models::RetrievedTicketbook;
use nym_credential_storage::models::{EmergencyCredential, RetrievedTicketbook};
use nym_credential_storage::storage::Storage;
use nym_credentials::ecash::bandwidth::CredentialSpendingData;
use nym_credentials_interface::{
@@ -220,6 +220,19 @@ impl<C, St: Storage> BandwidthController<C, St> {
}
}
}
pub async fn get_emergency_credential(
&self,
typ: &str,
) -> Result<Option<EmergencyCredential>, BandwidthControllerError>
where
<St as Storage>::StorageError: Send + Sync + 'static,
{
self.storage
.get_emergency_credential(typ)
.await
.map_err(BandwidthControllerError::credential_storage_error)
}
}
impl<C, St> Clone for BandwidthController<C, St>
+17
View File
@@ -11,6 +11,9 @@ use crate::{error::BandwidthControllerError, BandwidthController, PreparedCreden
pub const DEFAULT_TICKETS_TO_SPEND: u32 = 1;
// TODO: this does not really belong here
pub const UPGRADE_MODE_JWT_TYPE: &str = "UPGRADE_MODE_JWT";
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait BandwidthTicketProvider: Send + Sync {
@@ -20,6 +23,8 @@ pub trait BandwidthTicketProvider: Send + Sync {
gateway_id: ed25519::PublicKey,
tickets_to_spend: u32,
) -> Result<PreparedCredential, BandwidthControllerError>;
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError>;
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -39,4 +44,16 @@ where
self.prepare_ecash_ticket(ticket_type, gateway_id.to_bytes(), tickets_to_spend)
.await
}
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError> {
let Some(emergency_credential) =
self.get_emergency_credential(UPGRADE_MODE_JWT_TYPE).await?
else {
return Ok(None);
};
// upgrade mode credential is just a simple stringified JWT
let token = String::from_utf8(emergency_credential.data.content)
.map_err(|_| BandwidthControllerError::MalformedUpgradeModeToken)?;
Ok(Some(token))
}
}
@@ -81,6 +81,10 @@ pub struct CommonClientInitArgs {
#[cfg_attr(feature = "cli", clap(long, hide = true))]
pub enabled_credentials_mode: Option<bool>,
/// Change the default minimum node performance used during initial node selection filtering.
#[cfg_attr(feature = "cli", clap(long, hide = true))]
pub minimum_gateway_performance: Option<u8>,
/// Mostly debug-related option to increase default traffic rate so that you would not need to
/// modify config post init
#[cfg_attr(feature = "cli", clap(long, hide = true))]
@@ -173,10 +177,14 @@ where
})?;
hardcoded_topology.entry_capable_nodes().cloned().collect()
} else {
let minimum_performance = common_args
.minimum_gateway_performance
.unwrap_or(core.debug.topology.minimum_gateway_performance);
crate::init::helpers::gateways_for_init(
&core.client.nym_api_urls,
user_agent,
core.debug.topology.minimum_gateway_performance,
minimum_performance,
core.debug.topology.ignore_ingress_epoch_role,
None,
)
+1 -1
View File
@@ -45,7 +45,7 @@ pub enum ClientCoreError {
#[cfg(not(target_arch = "wasm32"))]
#[error("resolution failed: {0}")]
ResolutionFailed(#[from] nym_http_api_client::HickoryDnsError),
ResolutionFailed(#[from] nym_http_api_client::ResolveError),
#[error("no gateways on network")]
NoGatewaysOnNetwork,
@@ -30,7 +30,6 @@ pub(crate) async fn connect_async(
resolver
.resolve_str(domain)
.await?
.into_iter()
.map(|a| SocketAddr::new(a, port))
.collect()
}
@@ -88,3 +88,6 @@ features = ["js"]
[features]
wasm = []
[lints]
workspace = true
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use si_scale::helpers::bibytes2;
use std::fmt::{Display, Formatter};
use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
use std::sync::Arc;
use std::time::Duration;
@@ -26,6 +27,39 @@ pub struct ClientBandwidth {
inner: Arc<ClientBandwidthInner>,
}
// simple helper for logging purposes to accommodate 'unknown' case
pub(crate) enum UpgradeModeEnabledWrapper {
True,
False,
Unknown,
}
impl From<Option<bool>> for UpgradeModeEnabledWrapper {
fn from(value: Option<bool>) -> Self {
match value {
Some(true) => UpgradeModeEnabledWrapper::True,
Some(false) => UpgradeModeEnabledWrapper::False,
None => UpgradeModeEnabledWrapper::Unknown,
}
}
}
impl From<bool> for UpgradeModeEnabledWrapper {
fn from(value: bool) -> Self {
Some(value).into()
}
}
impl Display for UpgradeModeEnabledWrapper {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
UpgradeModeEnabledWrapper::True => write!(f, "true"),
UpgradeModeEnabledWrapper::False => write!(f, "false"),
UpgradeModeEnabledWrapper::Unknown => write!(f, "unknown"),
}
}
}
struct ClientBandwidthInner {
/// the actual bandwidth amount (in bytes) available
available: AtomicI64,
@@ -71,26 +105,41 @@ impl ClientBandwidth {
self.inner.available.load(Ordering::Acquire)
}
pub(crate) fn maybe_log_bandwidth(&self, now: Option<OffsetDateTime>) {
pub(crate) fn maybe_log_bandwidth(
&self,
now: Option<OffsetDateTime>,
upgrade_mode: impl Into<UpgradeModeEnabledWrapper>,
) {
let last = self.last_logged();
let now = now.unwrap_or_else(OffsetDateTime::now_utc);
if last + Duration::from_secs(10) < now {
self.log_bandwidth(Some(now))
self.log_bandwidth(Some(now), upgrade_mode)
}
}
pub(crate) fn log_bandwidth(&self, now: Option<OffsetDateTime>) {
pub(crate) fn log_bandwidth(
&self,
now: Option<OffsetDateTime>,
upgrade_mode: impl Into<UpgradeModeEnabledWrapper>,
) {
let now = now.unwrap_or_else(OffsetDateTime::now_utc);
let upgrade_mode = upgrade_mode.into();
let remaining = self.remaining();
let remaining_bi2 = bibytes2(remaining as f64);
if remaining < 0 {
tracing::warn!("OUT OF BANDWIDTH. remaining: {remaining_bi2}");
tracing::warn!(
"OUT OF BANDWIDTH. remaining: {remaining_bi2}. in 'upgrade mode': {upgrade_mode}"
);
} else if remaining < 1_000_000 {
tracing::info!("remaining bandwidth: {remaining_bi2}");
tracing::info!(
"remaining bandwidth: {remaining_bi2}. in 'upgrade mode': {upgrade_mode}"
);
} else {
tracing::debug!("remaining bandwidth: {remaining_bi2}");
tracing::trace!(
"remaining bandwidth: {remaining_bi2}. in 'upgrade mode': {upgrade_mode}"
);
}
self.inner
@@ -98,26 +147,35 @@ impl ClientBandwidth {
.store(now.unix_timestamp(), Ordering::Relaxed)
}
pub(crate) fn update_and_maybe_log(&self, remaining: i64) {
pub(crate) fn update_and_maybe_log(
&self,
remaining: i64,
upgrade_mode: impl Into<UpgradeModeEnabledWrapper>,
) {
let now = OffsetDateTime::now_utc();
self.inner.available.store(remaining, Ordering::Release);
self.inner
.last_updated_ts
.store(now.unix_timestamp(), Ordering::Relaxed);
self.maybe_log_bandwidth(Some(now))
self.maybe_log_bandwidth(Some(now), upgrade_mode)
}
pub(crate) fn update_and_log(&self, remaining: i64) {
pub(crate) fn update_and_log(
&self,
remaining: i64,
upgrade_mode: impl Into<UpgradeModeEnabledWrapper>,
) {
let now = OffsetDateTime::now_utc();
self.inner.available.store(remaining, Ordering::Release);
self.inner
.last_updated_ts
.store(now.unix_timestamp(), Ordering::Relaxed);
self.log_bandwidth(Some(now))
self.log_bandwidth(Some(now), upgrade_mode)
}
fn last_logged(&self) -> OffsetDateTime {
// SAFETY: this value is always populated with valid timestamps
#[allow(clippy::unwrap_used)]
OffsetDateTime::from_unix_timestamp(self.inner.last_logged_ts.load(Ordering::Relaxed))
.unwrap()
}
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::error::GatewayClientError;
use nym_network_defaults::TicketTypeRepr::V1MixnetEntry;
use nym_credentials_interface::DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD;
use si_scale::helpers::bibytes2;
use std::time::Duration;
@@ -103,7 +103,7 @@ impl BandwidthTickets {
// 20% of entry ticket value
pub const DEFAULT_REMAINING_BANDWIDTH_THRESHOLD: i64 =
(V1MixnetEntry.bandwidth_value() / 5) as i64;
DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD;
pub const DEFAULT_CUTOFF_REMAINING_BANDWIDTH_THRESHOLD: Option<i64> = None;
@@ -20,9 +20,9 @@ use nym_credentials_interface::TicketType;
use nym_crypto::asymmetric::ed25519;
use nym_gateway_requests::registration::handshake::client_handshake;
use nym_gateway_requests::{
BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersionExt,
GatewayRequestsError, SensitiveServerResponse, ServerResponse, SharedGatewayKey,
SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION,
BandwidthResponse, BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersion,
GatewayProtocolVersionExt, GatewayRequestsError, SensitiveServerResponse, ServerResponse,
SharedGatewayKey, SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION,
};
use nym_sphinx::forwarding::packet::MixPacket;
use nym_statistics_common::clients::connection::ConnectionStatsEvent;
@@ -101,8 +101,7 @@ pub struct GatewayClient<C, St = EphemeralCredentialStorage> {
bandwidth_controller: Option<BandwidthController<C, St>>,
stats_reporter: ClientStatsSender,
// currently unused (but populated)
negotiated_protocol: Option<u8>,
negotiated_protocol: Option<GatewayProtocolVersion>,
// Callback on the fd as soon as the connection has been established
#[cfg(unix)]
@@ -166,10 +165,12 @@ impl<C, St> GatewayClient<C, St> {
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::unreachable)]
async fn _close_connection(&mut self) -> Result<(), GatewayClientError> {
match std::mem::replace(&mut self.connection, SocketState::NotConnected) {
SocketState::Available(mut socket) => Ok((*socket).close(None).await?),
SocketState::PartiallyDelegated(_) => {
// SAFETY: this is only called after the caller has already recovered the connection
unreachable!("this branch should have never been reached!")
}
_ => Ok(()), // no need to do anything in those cases
@@ -177,6 +178,7 @@ impl<C, St> GatewayClient<C, St> {
}
#[cfg(target_arch = "wasm32")]
#[allow(clippy::unreachable)]
async fn _close_connection(&mut self) -> Result<(), GatewayClientError> {
match std::mem::replace(&mut self.connection, SocketState::NotConnected) {
SocketState::Available(socket) => {
@@ -184,6 +186,7 @@ impl<C, St> GatewayClient<C, St> {
Ok(())
}
SocketState::PartiallyDelegated(_) => {
// SAFETY: this is only called after the caller has already recovered the connection
unreachable!("this branch should have never been reached!")
}
_ => Ok(()), // no need to do anything in those cases
@@ -458,43 +461,16 @@ impl<C, St> GatewayClient<C, St> {
}
}
fn check_gateway_protocol(
&self,
gateway_protocol: Option<u8>,
) -> Result<(), GatewayClientError> {
debug!("gateway protocol: {gateway_protocol:?}, ours: {CURRENT_PROTOCOL_VERSION}");
// right now there are no failure cases here, but this might change in the future
match gateway_protocol {
None => {
warn!("the gateway we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0");
// note: in +1.2.0 we will have to return a hard error here
Ok(())
}
Some(v) if v > CURRENT_PROTOCOL_VERSION => {
let err = GatewayClientError::IncompatibleProtocol {
gateway: Some(v),
current: CURRENT_PROTOCOL_VERSION,
};
error!("{err}");
Err(err)
}
Some(_) => {
debug!("the gateway is using exactly the same (or older) protocol version as we are. We're good to continue!");
Ok(())
}
}
}
async fn register(
&mut self,
derive_aes256_gcm_siv_key: bool,
supported_gateway_protocol: Option<GatewayProtocolVersion>,
) -> Result<(), GatewayClientError> {
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
let derive_aes256_gcm_siv_key = supported_gateway_protocol.supports_aes256_gcm_siv();
debug_assert!(self.connection.is_available());
log::debug!(
"registering with gateway. using legacy key derivation: {}",
@@ -505,14 +481,13 @@ impl<C, St> GatewayClient<C, St> {
// and putting it into the GatewayClient struct would be a hassle
let mut rng = OsRng;
let shared_key = match &mut self.connection {
let handshake_result = match &mut self.connection {
SocketState::Available(ws_stream) => client_handshake(
&mut rng,
ws_stream,
self.local_identity.as_ref(),
self.gateway_identity,
self.cfg.bandwidth.require_tickets,
derive_aes256_gcm_siv_key,
supported_gateway_protocol,
#[cfg(not(target_arch = "wasm32"))]
self.shutdown_token.clone(),
)
@@ -521,26 +496,31 @@ impl<C, St> GatewayClient<C, St> {
_ => return Err(GatewayClientError::ConnectionInInvalidState),
}?;
let (authentication_status, gateway_protocol) = match self.read_control_response().await? {
let authentication_status = match self.read_control_response().await? {
ServerResponse::Register {
protocol_version,
status,
} => (status, protocol_version),
upgrade_mode,
..
} => {
if upgrade_mode {
warn!("the system is currently undergoing an upgrade. some of its functionalities might be unstable")
}
status
}
ServerResponse::Error { message } => {
return Err(GatewayClientError::GatewayError(message))
}
other => return Err(GatewayClientError::UnexpectedResponse { name: other.name() }),
};
self.check_gateway_protocol(gateway_protocol)?;
self.authenticated = authentication_status;
if self.authenticated {
self.shared_key = Some(Arc::new(shared_key));
self.shared_key = Some(Arc::new(handshake_result.derived_key));
}
// populate the negotiated protocol for future uses
self.negotiated_protocol = gateway_protocol;
self.negotiated_protocol = Some(handshake_result.negotiated_protocol);
Ok(())
}
@@ -623,13 +603,24 @@ impl<C, St> GatewayClient<C, St> {
protocol_version,
status,
bandwidth_remaining,
upgrade_mode,
} => {
self.check_gateway_protocol(protocol_version)?;
if protocol_version.is_future_version() {
// SAFETY: future version is always defined
#[allow(clippy::unwrap_used)]
let version = protocol_version.unwrap();
error!("the gateway insists on using v{version} protocol which is not supported by this client");
return Err(GatewayClientError::AuthenticationFailure);
}
self.authenticated = status;
self.bandwidth.update_and_maybe_log(bandwidth_remaining);
self.bandwidth
.update_and_maybe_log(bandwidth_remaining, upgrade_mode);
self.negotiated_protocol = protocol_version;
log::debug!("authenticated: {status}, bandwidth remaining: {bandwidth_remaining}");
if upgrade_mode {
warn!("the system is currently undergoing an upgrade. some of its functionalities might be unstable")
}
Ok(())
}
@@ -650,7 +641,7 @@ impl<C, St> GatewayClient<C, St> {
.public_key()
.derive_destination_address();
let msg = ClientControlRequest::new_authenticate(
let msg = ClientControlRequest::new_legacy_authenticate(
self_address,
shared_key,
self.cfg.bandwidth.require_tickets,
@@ -659,25 +650,40 @@ impl<C, St> GatewayClient<C, St> {
.await
}
async fn authenticate_v2(&mut self) -> Result<(), GatewayClientError> {
async fn authenticate_v2(
&mut self,
requested_protocol_version: GatewayProtocolVersion,
) -> Result<(), GatewayClientError> {
debug!("using v2 authentication");
let Some(shared_key) = self.shared_key.as_ref() else {
return Err(GatewayClientError::NoSharedKeyAvailable);
};
let msg = ClientControlRequest::new_authenticate_v2(shared_key, &self.local_identity)?;
let msg = ClientControlRequest::new_authenticate_v2(
shared_key,
&self.local_identity,
requested_protocol_version,
)?;
self.send_authenticate_request_and_handle_response(msg)
.await
}
async fn authenticate(&mut self, use_v2: bool) -> Result<(), GatewayClientError> {
async fn authenticate(
&mut self,
supported_gateway_protocol: Option<GatewayProtocolVersion>,
) -> Result<(), GatewayClientError> {
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
debug!("authenticating with gateway");
if use_v2 {
self.authenticate_v2().await
if supported_gateway_protocol.supports_authenticate_v2() {
// use the highest possible protocol version the gateway has announced support for
// SAFETY: if announced protocol supports auth v2, it means it's properly set
#[allow(clippy::unwrap_used)]
self.authenticate_v2(supported_gateway_protocol.unwrap())
.await
} else {
self.authenticate_v1().await
}
@@ -708,9 +714,12 @@ impl<C, St> GatewayClient<C, St> {
}
};
debug!("supported gateway protocol: {gw_protocol:?}");
let supports_aes_gcm_siv = gw_protocol.supports_aes256_gcm_siv();
let supports_auth_v2 = gw_protocol.supports_authenticate_v2();
let supports_key_rotation_info = gw_protocol.supports_key_rotation_packet();
let supports_upgrade_mode = gw_protocol.supports_upgrade_mode();
if !supports_aes_gcm_siv {
warn!("this gateway is on an old version that doesn't support AES256-GCM-SIV");
@@ -721,6 +730,16 @@ impl<C, St> GatewayClient<C, St> {
if !supports_key_rotation_info {
warn!("this gateway is on an old version that doesn't support key rotation packets")
}
if !supports_upgrade_mode {
warn!("this gateway is on an old version that doesn't support upgrade mode")
}
let gw_protocol = if gw_protocol.is_future_version() {
warn!("we're running outdated software as gateway is announcing protocol {gw_protocol:?} whilst we're using {}. we're going to attempt to downgrade", GatewayProtocolVersion::CURRENT);
Some(GatewayProtocolVersion::CURRENT)
} else {
gw_protocol
};
if self.authenticated {
debug!("Already authenticated");
@@ -735,10 +754,11 @@ impl<C, St> GatewayClient<C, St> {
}
if self.shared_key.is_some() {
self.authenticate(supports_auth_v2).await?;
self.authenticate(gw_protocol).await?;
if self.authenticated {
// if we are authenticated it means we MUST have an associated shared_key
#[allow(clippy::unwrap_used)]
let shared_key = self.shared_key.as_ref().unwrap();
let requires_key_upgrade = shared_key.is_legacy() && supports_aes_gcm_siv;
@@ -751,9 +771,10 @@ impl<C, St> GatewayClient<C, St> {
Err(GatewayClientError::AuthenticationFailure)
}
} else {
self.register(supports_aes_gcm_siv).await?;
self.register(gw_protocol).await?;
// if registration didn't return an error, we MUST have an associated shared key
#[allow(clippy::unwrap_used)]
let shared_key = self.shared_key.as_ref().unwrap();
// we're always registering with the highest supported protocol,
@@ -783,51 +804,81 @@ impl<C, St> GatewayClient<C, St> {
}
}
async fn claim_ecash_bandwidth(
async fn wait_for_bandwidth_response(
&mut self,
credential: CredentialSpendingData,
) -> Result<(), GatewayClientError> {
let msg = ClientControlRequest::new_enc_ecash_credential(
credential,
self.shared_key.as_ref().unwrap(),
)?;
let bandwidth_remaining = match self
msg: ClientControlRequest,
) -> Result<BandwidthResponse, GatewayClientError> {
let response = match self
.send_websocket_message_with_non_send_response(msg)
.await?
{
ServerResponse::Bandwidth { available_total } => Ok(available_total),
ServerResponse::Bandwidth(response) => {
if response.upgrade_mode {
info!("the system is currently undergoing an upgrade. our bandwidth shouldn't have been metered")
}
Ok(response)
}
ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)),
ServerResponse::TypedError { error } => {
Err(GatewayClientError::TypedGatewayError(error))
}
other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }),
}?;
Ok(response)
}
async fn claim_ecash_bandwidth(
&mut self,
credential: CredentialSpendingData,
) -> Result<(), GatewayClientError> {
// SAFETY: claiming ecash bandwidth is called as part of `claim_bandwidth` which
// ensures the shared key is defined
#[allow(clippy::unwrap_used)]
let msg = ClientControlRequest::new_enc_ecash_credential(
credential,
self.shared_key.as_ref().unwrap(),
)?;
let response = self.wait_for_bandwidth_response(msg).await?;
// TODO: create tracing span
info!("managed to claim ecash bandwidth");
self.bandwidth.update_and_log(bandwidth_remaining);
self.bandwidth
.update_and_log(response.available_total, response.upgrade_mode);
Ok(())
}
pub async fn send_upgrade_mode_jwt(&mut self, token: String) -> Result<(), GatewayClientError> {
let msg = ClientControlRequest::new_upgrade_mode_jwt(token);
let response = self.wait_for_bandwidth_response(msg).await?;
// if gateway rejected our jwt, we would have returned an error
info!("gateway has accepted our jwt");
if !response.upgrade_mode {
error!("but we're not in upgrade mode - something is wrong!");
return Err(GatewayClientError::UnexpectedUpgradeModeState);
}
self.bandwidth
.update_and_log(response.available_total, response.upgrade_mode);
Ok(())
}
async fn try_claim_testnet_bandwidth(&mut self) -> Result<(), GatewayClientError> {
let msg = ClientControlRequest::ClaimFreeTestnetBandwidth;
let bandwidth_remaining = match self
.send_websocket_message_with_non_send_response(msg)
.await?
{
ServerResponse::Bandwidth { available_total } => Ok(available_total),
ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)),
other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }),
}?;
let response = self.wait_for_bandwidth_response(msg).await?;
info!("managed to claim testnet bandwidth");
self.bandwidth.update_and_log(bandwidth_remaining);
self.bandwidth
.update_and_log(response.available_total, response.upgrade_mode);
Ok(())
}
fn unchecked_bandwidth_controller(&self) -> &BandwidthController<C, St> {
// this is an unchecked method
#[allow(clippy::unwrap_used)]
self.bandwidth_controller.as_ref().unwrap()
}
@@ -919,6 +970,7 @@ impl<C, St> GatewayClient<C, St> {
BinaryRequest::ForwardSphinx { packet }
};
#[allow(clippy::expect_used)]
req.into_ws_message(
self.shared_key
.as_ref()
@@ -1025,6 +1077,8 @@ impl<C, St> GatewayClient<C, St> {
self.send_with_reconnection_on_failure(msg).await
}
// SAFETY: this method is only called when the connection is in `PartiallyDelegated` state
#[allow(clippy::unreachable)]
async fn recover_socket_connection(&mut self) -> Result<(), GatewayClientError> {
if self.connection.is_available() {
return Ok(());
@@ -1054,6 +1108,7 @@ impl<C, St> GatewayClient<C, St> {
return Err(GatewayClientError::ConnectionInInvalidState);
}
#[allow(clippy::expect_used)]
let partially_delegated =
match std::mem::replace(&mut self.connection, SocketState::Invalid) {
SocketState::Available(conn) => {
@@ -1069,7 +1124,13 @@ impl<C, St> GatewayClient<C, St> {
self.shutdown_token.clone(),
)
}
_ => unreachable!(),
other => {
error!(
"attempted to start mixnet listener whilst the connection is in {} state!",
other.name()
);
return Err(GatewayClientError::ConnectionInInvalidState);
}
};
self.connection = SocketState::PartiallyDelegated(partially_delegated);
@@ -1082,8 +1143,7 @@ impl<C, St> GatewayClient<C, St> {
}
// if we're reconnecting, because we lost connection, we need to re-authenticate the connection
self.authenticate(self.negotiated_protocol.supports_authenticate_v2())
.await?;
self.authenticate(self.negotiated_protocol).await?;
// this call is NON-blocking
self.start_listening_for_mixnet_messages()?;
@@ -39,7 +39,6 @@ pub(crate) async fn connect_async(
resolver
.resolve_str(domain)
.await?
.into_iter()
.map(|a| SocketAddr::new(a, port))
.collect()
}
@@ -54,7 +54,7 @@ pub enum GatewayClientError {
#[cfg(not(target_arch = "wasm32"))]
#[error("resolution failed: {0}")]
ResolutionFailed(#[from] nym_http_api_client::HickoryDnsError),
ResolutionFailed(#[from] nym_http_api_client::ResolveError),
#[error("No shared key was provided or obtained")]
NoSharedKeyAvailable,
@@ -128,6 +128,9 @@ pub enum GatewayClientError {
"this operation couldn't be completed as the program is in the process of shutting down"
)]
ShutdownInProgress,
#[error("the system is an unexpected upgrade mode state")]
UnexpectedUpgradeModeState,
}
impl From<WsError> for GatewayClientError {
@@ -35,6 +35,7 @@ impl PacketRouter {
}
}
#[allow(clippy::panic)]
pub fn route_mixnet_messages(
&self,
received_messages: Vec<Vec<u8>>,
@@ -54,6 +55,7 @@ impl PacketRouter {
Ok(())
}
#[allow(clippy::panic)]
pub fn route_acks(&self, received_acks: Vec<Vec<u8>>) -> Result<(), GatewayClientError> {
if let Err(err) = self.ack_sender.unbounded_send(received_acks) {
// check if the failure is due to the shutdown being in progress and thus the receiver channel
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::bandwidth::ClientBandwidth;
use crate::client::config::BandwidthTickets;
use crate::error::GatewayClientError;
use crate::packet_router::PacketRouter;
use crate::traits::GatewayPacketRouter;
@@ -10,7 +11,9 @@ use futures::channel::oneshot;
use futures::stream::{SplitSink, SplitStream};
use futures::{SinkExt, StreamExt};
use nym_gateway_requests::shared_key::SharedGatewayKey;
use nym_gateway_requests::{SensitiveServerResponse, ServerResponse, SimpleGatewayRequestsError};
use nym_gateway_requests::{
SendResponse, SensitiveServerResponse, ServerResponse, SimpleGatewayRequestsError,
};
use nym_task::ShutdownToken;
use si_scale::helpers::bibytes2;
use std::os::raw::c_int as RawFd;
@@ -154,11 +157,12 @@ impl PartiallyDelegatedRouter {
fn handle_text_message(&self, text: String) -> Result<(), GatewayClientError> {
// if we fail to deserialise the response, return a hard error. we can't handle garbage
match ServerResponse::try_from(text).map_err(|_| GatewayClientError::MalformedResponse)? {
ServerResponse::Send {
ServerResponse::Send(SendResponse {
remaining_bandwidth,
} => {
upgrade_mode,
}) => {
self.client_bandwidth
.update_and_maybe_log(remaining_bandwidth);
.update_and_maybe_log(remaining_bandwidth, upgrade_mode);
Ok(())
}
ServerResponse::Error { message } => {
@@ -174,7 +178,20 @@ impl PartiallyDelegatedRouter {
let available_bi2 = bibytes2(available as f64);
let required_bi2 = bibytes2(required as f64);
warn!("run out of bandwidth when attempting to send the message! we got {available_bi2} available, but needed at least {required_bi2} to send the previous message");
self.client_bandwidth.update_and_log(available);
// if we run out of bandwidth (and tried to send reasonable amount of data),
// the upgrade mode is implicitly disabled, as otherwise we would have been
// to proceed
let upgrade_mode = if available
< BandwidthTickets::DEFAULT_REMAINING_BANDWIDTH_THRESHOLD
{
Some(false)
} else {
// we were attempting to send a lot of data at once
// - we have no certainty about upgrade mode at this point
None
};
self.client_bandwidth
.update_and_log(available, upgrade_mode);
// UNIMPLEMENTED: we should stop sending messages until we recover bandwidth
Ok(())
}
@@ -327,6 +344,7 @@ impl PartiallyDelegatedHandle {
Ok(self.sink_half.send_all(&mut send_stream).await?)
}
#[allow(clippy::panic)]
pub(crate) async fn merge(self) -> Result<WsConn, GatewayClientError> {
let (mut stream_receiver, notify) = self.delegated_stream;
@@ -355,8 +373,10 @@ impl PartiallyDelegatedHandle {
// in receive_res
.map_err(|_| GatewayClientError::ConnectionAbruptlyClosed)?;
let stream = stream_results?;
// the error is thrown when trying to reunite sink and stream that did not originate
// from the same split which is impossible to happen here
#[allow(clippy::unwrap_used)]
Ok(self.sink_half.reunite(stream).unwrap())
}
}
@@ -387,4 +407,13 @@ impl SocketState {
SocketState::Available(_) | SocketState::PartiallyDelegated(_)
)
}
pub(crate) fn name(&self) -> &'static str {
match self {
SocketState::Available(_) => "available",
SocketState::PartiallyDelegated(_) => "partially delegated",
SocketState::NotConnected => "not connected",
SocketState::Invalid => "invalid",
}
}
}
+19
View File
@@ -1,6 +1,7 @@
// Copyright 2025 Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_crypto::asymmetric::ed25519;
use nym_ecash_signer_check::SignerCheckError;
use nym_validator_client::coconut::EcashApiError;
use nym_validator_client::nym_api::{EpochId, error::NymAPIError};
@@ -168,6 +169,24 @@ pub enum CredentialProxyError {
device_id: String,
credential_id: String,
},
#[error(
"the attestation check url has not been provided through either the CLI nor the default .env config"
)]
AttestationCheckUrlNotSet,
#[error("the provided attester public key is malformed: {source}")]
MalformedAttestationCheckUrl { source: url::ParseError },
#[error(
"the attester public key has not been provided through either the CLI nor the default .env config"
)]
AttesterPublicKeyNotSet,
#[error("the provided attester public key is malformed: {source}")]
MalformedAttesterPublicKey {
source: ed25519::Ed25519RecoveryError,
},
}
impl From<NymAPIError> for CredentialProxyError {
+1
View File
@@ -14,6 +14,7 @@ bincode = { workspace = true, optional = true }
log = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true, features = ["derive"], optional = true }
time = { workspace = true }
tokio = { workspace = true, features = ["sync"] }
zeroize = { workspace = true, features = ["zeroize_derive"] }
@@ -0,0 +1,17 @@
/*
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
CREATE TABLE emergency_credential
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL,
-- don't define any strict schema on the content as it might be implementation-dependant
content BLOB NOT NULL,
expiration TIMESTAMP WITHOUT TIME ZONE
);
-- no point in allowing duplicate data
CREATE UNIQUE INDEX emergency_credential_unique_type_content
ON emergency_credential (type, content);
@@ -1,7 +1,10 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook};
use crate::models::{
BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent,
RetrievedPendingTicketbook, RetrievedTicketbook,
};
use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature;
use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature;
use nym_compact_ecash::VerificationKeyAuth;
@@ -22,6 +25,12 @@ pub struct MemoryEcachTicketbookManager {
inner: Arc<RwLock<EcashCredentialManagerInner>>,
}
#[derive(Default)]
struct InternalIdCounters {
next_ticketbook_id: i64,
next_emergency_credential_id: i64,
}
#[derive(Default)]
struct EcashCredentialManagerInner {
ticketbooks: HashMap<i64, RetrievedTicketbook>,
@@ -29,13 +38,22 @@ struct EcashCredentialManagerInner {
master_vk: HashMap<u64, VerificationKeyAuth>,
coin_indices_sigs: HashMap<u64, Vec<AnnotatedCoinIndexSignature>>,
expiration_date_sigs: HashMap<(u64, Date), Vec<AnnotatedExpirationDateSignature>>,
_next_id: i64,
emergency_credentials: HashMap<String, Vec<EmergencyCredential>>,
// internal counters emulating assignment of an increasing id to new inserted database entries
internal_counters: InternalIdCounters,
}
impl EcashCredentialManagerInner {
fn next_id(&mut self) -> i64 {
let next = self._next_id;
self._next_id += 1;
fn next_ticketbook_id(&mut self) -> i64 {
let next = self.internal_counters.next_ticketbook_id;
self.internal_counters.next_ticketbook_id += 1;
next
}
fn next_emergency_credential_id(&mut self) -> i64 {
let next = self.internal_counters.next_emergency_credential_id;
self.internal_counters.next_emergency_credential_id += 1;
next
}
}
@@ -170,7 +188,7 @@ impl MemoryEcachTicketbookManager {
used_tickets: u32,
) {
let mut guard = self.inner.write().await;
let id = guard.next_id();
let id = guard.next_ticketbook_id();
#[allow(clippy::unwrap_used)]
let mut nasty_clone = hack_clone_ticketbook(ticketbook);
@@ -277,4 +295,41 @@ impl MemoryEcachTicketbookManager {
sigs.signatures.clone(),
);
}
pub(crate) async fn get_emergency_credential(&self, typ: &str) -> Option<EmergencyCredential> {
let guard = self.inner.read().await;
guard.emergency_credentials.get(typ)?.first().cloned()
}
pub(crate) async fn insert_emergency_credential(
&self,
credential: &EmergencyCredentialContent,
) {
let mut guard = self.inner.write().await;
let id = guard.next_emergency_credential_id();
guard
.emergency_credentials
.entry(credential.typ.clone())
.or_default()
.push(EmergencyCredential {
id,
data: credential.clone(),
});
}
pub(crate) async fn remove_emergency_credential(&self, id: i64) {
let mut guard = self.inner.write().await;
guard.emergency_credentials.retain(|_, credentials| {
credentials.retain(|c| c.id != id);
!credentials.is_empty()
})
}
pub(crate) async fn remove_emergency_credentials_of_type(&self, typ: &str) {
let mut guard = self.inner.write().await;
guard.emergency_credentials.remove(typ);
}
}
@@ -2,8 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
use crate::models::{
BasicTicketbookInformation, RawCoinIndexSignatures, RawExpirationDateSignatures,
RawVerificationKey, StoredIssuedTicketbook, StoredPendingTicketbook,
BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent,
RawCoinIndexSignatures, RawExpirationDateSignatures, RawVerificationKey,
StoredIssuedTicketbook, StoredPendingTicketbook,
};
use nym_ecash_time::Date;
use sqlx::{Executor, Sqlite, Transaction};
@@ -305,6 +306,74 @@ impl SqliteEcashTicketbookManager {
.await?;
Ok(())
}
pub(crate) async fn get_emergency_credential(
&self,
typ: &str,
) -> Result<Option<EmergencyCredential>, sqlx::Error> {
sqlx::query_as(
r#"
SELECT *
FROM emergency_credential
WHERE type = ?
AND (expiration IS NULL OR expiration > CURRENT_TIMESTAMP)
ORDER BY expiration DESC NULLS LAST
LIMIT 1
"#,
)
.bind(typ)
.fetch_optional(&*self.connection_pool)
.await
}
pub(crate) async fn insert_emergency_credential(
&self,
credential: &EmergencyCredentialContent,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO emergency_credential
(type, content, expiration)
VALUES (?, ?, ?)
ON CONFLICT(type, content) DO NOTHING;
"#,
credential.typ,
credential.content,
credential.expiration,
)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn remove_emergency_credential(&self, id: i64) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
DELETE FROM emergency_credential
WHERE id = ?
"#,
id
)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn remove_emergency_credentials_of_type(
&self,
typ: &str,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
DELETE FROM emergency_credential
WHERE type = ?
"#,
typ
)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
}
pub(crate) async fn get_next_unspent_ticketbook<'a, E>(
@@ -3,7 +3,10 @@
use crate::backends::memory::MemoryEcachTicketbookManager;
use crate::error::StorageError;
use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook};
use crate::models::{
BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent,
RetrievedPendingTicketbook, RetrievedTicketbook,
};
use crate::storage::Storage;
use async_trait::async_trait;
use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature;
@@ -218,6 +221,38 @@ impl Storage for EphemeralStorage {
.await;
Ok(())
}
async fn get_emergency_credential(
&self,
typ: &str,
) -> Result<Option<EmergencyCredential>, Self::StorageError> {
Ok(self.storage_manager.get_emergency_credential(typ).await)
}
async fn insert_emergency_credential(
&self,
credential: &EmergencyCredentialContent,
) -> Result<(), Self::StorageError> {
self.storage_manager
.insert_emergency_credential(credential)
.await;
Ok(())
}
async fn remove_emergency_credential(&self, id: i64) -> Result<(), Self::StorageError> {
self.storage_manager.remove_emergency_credential(id).await;
Ok(())
}
async fn remove_emergency_credentials_of_type(
&self,
typ: &str,
) -> Result<(), Self::StorageError> {
self.storage_manager
.remove_emergency_credentials_of_type(typ)
.await;
Ok(())
}
}
#[cfg(test)]
+18
View File
@@ -3,6 +3,7 @@
use nym_credentials::{IssuanceTicketBook, IssuedTicketBook};
use nym_ecash_time::Date;
use time::OffsetDateTime;
use zeroize::{Zeroize, ZeroizeOnDrop};
pub struct RetrievedTicketbook {
@@ -78,3 +79,20 @@ pub struct RawVerificationKey {
pub serialised_key: Vec<u8>,
pub serialization_revision: u8,
}
#[derive(Clone, Debug)]
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
pub struct EmergencyCredential {
pub id: i64,
#[cfg_attr(not(target_arch = "wasm32"), sqlx(flatten))]
pub data: EmergencyCredentialContent,
}
#[derive(Clone, Debug)]
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
pub struct EmergencyCredentialContent {
#[cfg_attr(not(target_arch = "wasm32"), sqlx(rename = "type"))]
pub typ: String,
pub content: Vec<u8>,
pub expiration: Option<OffsetDateTime>,
}
@@ -3,6 +3,7 @@
mod legacy_helpers;
use crate::models::{EmergencyCredential, EmergencyCredentialContent};
use crate::{
backends::sqlite::{
get_next_unspent_ticketbook, increase_used_ticketbook_tickets, SqliteEcashTicketbookManager,
@@ -401,4 +402,36 @@ impl Storage for PersistentStorage {
.await?;
Ok(())
}
async fn get_emergency_credential(
&self,
typ: &str,
) -> Result<Option<EmergencyCredential>, Self::StorageError> {
Ok(self.storage_manager.get_emergency_credential(typ).await?)
}
async fn insert_emergency_credential(
&self,
credential: &EmergencyCredentialContent,
) -> Result<(), Self::StorageError> {
self.storage_manager
.insert_emergency_credential(credential)
.await?;
Ok(())
}
async fn remove_emergency_credential(&self, id: i64) -> Result<(), Self::StorageError> {
self.storage_manager.remove_emergency_credential(id).await?;
Ok(())
}
async fn remove_emergency_credentials_of_type(
&self,
typ: &str,
) -> Result<(), Self::StorageError> {
self.storage_manager
.remove_emergency_credentials_of_type(typ)
.await?;
Ok(())
}
}
+21 -1
View File
@@ -1,7 +1,10 @@
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook};
use crate::models::{
BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent,
RetrievedPendingTicketbook, RetrievedTicketbook,
};
use async_trait::async_trait;
use nym_compact_ecash::VerificationKeyAuth;
use nym_credentials::ecash::bandwidth::serialiser::keys::EpochVerificationKey;
@@ -108,4 +111,21 @@ pub trait Storage: Clone + Send + Sync {
&self,
signatures: &AggregatedExpirationDateSignatures,
) -> Result<(), Self::StorageError>;
async fn get_emergency_credential(
&self,
typ: &str,
) -> Result<Option<EmergencyCredential>, Self::StorageError>;
async fn insert_emergency_credential(
&self,
credential: &EmergencyCredentialContent,
) -> Result<(), Self::StorageError>;
async fn remove_emergency_credential(&self, id: i64) -> Result<(), Self::StorageError>;
async fn remove_emergency_credentials_of_type(
&self,
typ: &str,
) -> Result<(), Self::StorageError>;
}
+2 -1
View File
@@ -17,7 +17,6 @@ cosmwasm-std = { workspace = true }
cw-utils = { workspace = true }
dyn-clone = { workspace = true }
futures = { workspace = true }
rand = { workspace = true }
si-scale = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
@@ -27,8 +26,10 @@ tracing = { workspace = true }
nym-api-requests = { path = "../../nym-api/nym-api-requests" }
nym-credentials = { path = "../credentials" }
nym-credentials-interface = { path = "../credentials-interface" }
nym-crypto = { path = "../crypto", features = ["asymmetric"] }
nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" }
nym-gateway-requests = { path = "../gateway-requests" }
nym-gateway-storage = { path = "../gateway-storage" }
nym-task = { path = "../task" }
nym-validator-client = { path = "../client-libs/validator-client" }
nym-upgrade-mode-check = { path = "../upgrade-mode-check" }
@@ -6,7 +6,6 @@ use crate::ClientBandwidth;
use crate::error::*;
use nym_credentials::ecash::utils::ecash_today;
use nym_credentials_interface::Bandwidth;
use nym_gateway_requests::ServerResponse;
use nym_gateway_storage::traits::BandwidthGatewayStorage;
use si_scale::helpers::bibytes2;
use time::OffsetDateTime;
@@ -66,7 +65,7 @@ impl BandwidthStorageManager {
Ok(())
}
pub async fn handle_claim_testnet_bandwidth(&mut self) -> Result<ServerResponse> {
pub async fn handle_claim_testnet_bandwidth(&mut self) -> Result<i64> {
debug!("handling testnet bandwidth request");
if self.only_coconut_credentials {
@@ -76,8 +75,7 @@ impl BandwidthStorageManager {
self.increase_bandwidth(FREE_TESTNET_BANDWIDTH_VALUE, ecash_today())
.await?;
let available_total = self.client_bandwidth.available().await;
Ok(ServerResponse::Bandwidth { available_total })
Ok(available_total)
}
#[instrument(skip_all)]
@@ -96,7 +94,7 @@ impl BandwidthStorageManager {
let available_bi2 = bibytes2(available_bandwidth as f64);
let required_bi2 = bibytes2(required_bandwidth as f64);
debug!(available = available_bi2, required = required_bi2);
trace!(available = available_bi2, required = required_bi2);
self.consume_bandwidth(required_bandwidth).await?;
let remaining_bandwidth = self.client_bandwidth.available().await;
@@ -47,6 +47,25 @@ pub enum Error {
UnknownTicketType(#[from] nym_credentials_interface::UnknownTicketType),
}
impl Error {
pub fn is_out_of_bandwidth(&self) -> bool {
matches!(self, Error::OutOfBandwidth { .. })
}
}
pub trait OutOfBandwidthResultExt {
fn is_out_of_bandwidth(&self) -> bool;
}
impl<T> OutOfBandwidthResultExt for Result<T> {
fn is_out_of_bandwidth(&self) -> bool {
match &self {
Ok(_) => false,
Err(err) => err.is_out_of_bandwidth(),
}
}
}
impl From<EcashTicketError> for Error {
fn from(err: EcashTicketError) -> Self {
// don't expose storage issue details to the user
@@ -13,11 +13,13 @@ use tracing::*;
pub use client_bandwidth::*;
pub use error::*;
pub use upgrade_mode::UpgradeModeState;
pub mod bandwidth_storage_manager;
mod client_bandwidth;
pub mod ecash;
pub mod error;
pub mod upgrade_mode;
pub struct CredentialVerifier {
credential: CredentialSpendingRequest,
@@ -0,0 +1,293 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
use nym_crypto::asymmetric::ed25519;
use nym_upgrade_mode_check::{
CREDENTIAL_PROXY_JWT_ISSUER, UpgradeModeAttestation, validate_upgrade_mode_jwt,
};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
use std::time::Duration;
use thiserror::Error;
use time::OffsetDateTime;
use tokio::sync::{Notify, RwLock};
use tracing::{debug, error, info};
#[derive(Debug, Error)]
pub enum UpgradeModeEnableError {
#[error("too soon to perform another upgrade mode attestation check")]
TooManyRecheckRequests,
#[error("provided upgrade mode JWT is invalid: {0}")]
InvalidUpgradeModeJWT(#[from] nym_upgrade_mode_check::UpgradeModeCheckError),
#[error("the upgrade mode attestation does not appear to have been published")]
AttestationNotPublished,
#[error("the provided upgrade mode attestation is different from the published one")]
MismatchedUpgradeModeAttestation,
}
// the idea behind this is as follows:
// it's been relatively a long time since the watcher last performed its checks (since it's in 'regular' mode)
// and some client has just sent a JWT. we have to retrieve most recent information in case upgrade mode
// has just been enabled, and we haven't learned about it yet
#[derive(Clone)]
pub struct UpgradeModeCheckRequestSender(Option<UnboundedSender<CheckRequest>>);
impl UpgradeModeCheckRequestSender {
pub fn new(sender: UnboundedSender<CheckRequest>) -> Self {
UpgradeModeCheckRequestSender(Some(sender))
}
pub fn new_empty() -> Self {
Self(None)
}
pub(crate) fn send_request(&self, on_done: Arc<Notify>) {
let Some(ref inner) = self.0 else {
// make sure the caller gets notified so it doesn't wait forever
on_done.notify_waiters();
return;
};
if let Err(not_sent) = inner.unbounded_send(CheckRequest { on_done }) {
debug!("failed to send upgrade mode check request - {not_sent}");
// make sure the caller gets notified so it doesn't wait forever
not_sent.into_inner().on_done.notify_waiters();
}
}
}
pub type UpgradeModeCheckRequestReceiver = UnboundedReceiver<CheckRequest>;
pub struct CheckRequest {
on_done: Arc<Notify>,
}
impl CheckRequest {
pub fn finalize(self) {
self.on_done.notify_waiters();
}
}
#[derive(Clone, Copy)]
pub struct UpgradeModeCheckConfig {
/// The minimum duration since the last explicit check to allow creation of separate request.
pub min_staleness_recheck: Duration,
}
/// Full upgrade mode information, that apart from boolean flag indicating the state
/// and the attestation information, includes channel connection to relevant
/// attestation watcher to request state rechecks
#[derive(Clone)]
pub struct UpgradeModeDetails {
pub(crate) config: UpgradeModeCheckConfig,
pub(crate) request_checker: UpgradeModeCheckRequestSender,
pub(crate) state: UpgradeModeState,
}
impl UpgradeModeDetails {
pub fn new(
config: UpgradeModeCheckConfig,
request_checker: UpgradeModeCheckRequestSender,
state: UpgradeModeState,
) -> Self {
UpgradeModeDetails {
config,
request_checker,
state,
}
}
pub fn state(&self) -> &UpgradeModeState {
&self.state
}
pub fn enabled(&self) -> bool {
self.state.upgrade_mode_enabled()
}
fn since_last_query(&self) -> Duration {
self.state.since_last_query()
}
pub fn can_request_recheck(&self) -> bool {
self.since_last_query() > self.config.min_staleness_recheck
}
// explicitly request state update. this is only called when upgrade mode is NOT enabled,
// and client has sent a JWT instead of ticket
async fn request_recheck(&self) -> bool {
// send request
let on_done = Arc::new(Notify::new());
self.request_checker.send_request(on_done.clone());
// wait for response - note, if we fail to send, notification will be sent regardless,
// so that we wouldn't get stuck in here
on_done.notified().await;
// check the state again
self.enabled()
}
pub async fn try_enable_via_received_jwt(
&self,
token: String,
) -> Result<(), UpgradeModeEnableError> {
// see if it's viable to perform another expedited check
if !self.can_request_recheck() {
return Err(UpgradeModeEnableError::TooManyRecheckRequests);
}
// first validate whether the received JWT is even valid
let attestation = validate_upgrade_mode_jwt(&token, Some(CREDENTIAL_PROXY_JWT_ISSUER))?;
// send request to revalidate internal state
// this will, among other things, pull fresh attestation from the configured endpoint
// and also verify required signatures (and pubkeys)
self.request_recheck().await;
// not strictly necessary, but check if provided attestation actually matches the one retrieved
// (if any)
let Some(retrieved_attestation) = self.state.attestation().await else {
return Err(UpgradeModeEnableError::AttestationNotPublished);
};
if retrieved_attestation != attestation {
return Err(UpgradeModeEnableError::MismatchedUpgradeModeAttestation);
}
// note: if attestation has been returned, it means we're definitely in upgrade mode
// (otherwise it wouldn't have existed in the state)
info!("managed to initialise upgrade mode through received JWT");
Ok(())
}
}
/// Detailed upgrade mode information, that apart from boolean flag,
/// also includes, if applicable, the associated attestation
#[derive(Clone)]
pub struct UpgradeModeState {
inner: Arc<UpgradeModeStateInner>,
}
/// Just a shareable flag to indicate whether upgrade mode is enabled or disabled
#[derive(Clone, Default)]
pub struct UpgradeModeStatus(Arc<AtomicBool>);
impl UpgradeModeStatus {
pub fn enabled(&self) -> bool {
self.0.load(Ordering::Acquire)
}
pub fn enable(&self) {
self.0.store(true, Ordering::Relaxed);
}
pub fn disable(&self) {
self.0.store(false, Ordering::Release);
}
}
impl UpgradeModeState {
pub fn new(attester_public_key: ed25519::PublicKey) -> UpgradeModeState {
UpgradeModeState {
inner: Arc::new(UpgradeModeStateInner {
expected_attester_public_key: attester_public_key,
expected_attestation: RwLock::new(None),
last_queried_ts: AtomicI64::new(OffsetDateTime::UNIX_EPOCH.unix_timestamp()),
status: UpgradeModeStatus(Arc::new(AtomicBool::new(false))),
}),
}
}
pub fn attester_pubkey(&self) -> ed25519::PublicKey {
self.inner.expected_attester_public_key
}
pub async fn attestation(&self) -> Option<UpgradeModeAttestation> {
self.inner.expected_attestation.read().await.clone()
}
pub async fn try_set_expected_attestation(
&self,
expected_attestation: Option<UpgradeModeAttestation>,
) {
// make sure to only enable upgrade mode flag AFTER we have written the expected value
// (or still hold the exclusive lock as in this instance)
let mut guard = self.inner.expected_attestation.write().await;
// ensure that the attestation had been signed with the expected key
if let Some(attestation) = expected_attestation.as_ref() {
if attestation.content.attester_public_key != self.inner.expected_attester_public_key {
self.update_last_queried(OffsetDateTime::now_utc());
return;
}
self.enable_upgrade_mode()
} else {
self.disable_upgrade_mode()
}
self.update_last_queried(OffsetDateTime::now_utc());
*guard = expected_attestation;
}
pub fn upgrade_mode_status(&self) -> UpgradeModeStatus {
self.inner.status.clone()
}
pub fn upgrade_mode_enabled(&self) -> bool {
self.inner.status.enabled()
}
pub fn enable_upgrade_mode(&self) {
self.inner.status.enable()
}
pub fn disable_upgrade_mode(&self) {
self.inner.status.disable()
}
pub fn last_queried(&self) -> OffsetDateTime {
// SAFETY: the stored value here is always a valid unix timestamp
#[allow(clippy::unwrap_used)]
OffsetDateTime::from_unix_timestamp(self.inner.last_queried_ts.load(Ordering::Acquire))
.unwrap()
}
pub fn update_last_queried(&self, queried_at: OffsetDateTime) {
self.inner
.last_queried_ts
.store(queried_at.unix_timestamp(), Ordering::Release);
}
pub fn since_last_query(&self) -> Duration {
(OffsetDateTime::now_utc() - self.last_queried())
.try_into()
.unwrap_or_else(|_| {
error!("somehow our last query for upgrade mode was in the future!");
Duration::ZERO
})
}
}
struct UpgradeModeStateInner {
/// Expected public key of the entity issuing upgrade mode attestations.
expected_attester_public_key: ed25519::PublicKey,
/// Contents of the published upgrade mode attestation, as queried by this node
expected_attestation: RwLock<Option<UpgradeModeAttestation>>,
/// timestamp indicating last time this node has queried for the current upgrade mode attestation
/// it is used to determine if an additional expedited query should be made in case client sends a JWT
/// whilst this node is not aware of the upgrade mode
last_queried_ts: AtomicI64,
/// flag indicating whether upgrade mode is currently enabled. this is to perform cheap checks
/// that avoid having to acquire the lock
// (and dealing with the async consequences of that)
status: UpgradeModeStatus,
}
+2
View File
@@ -23,3 +23,5 @@ rand = { workspace = true }
nym-compact-ecash = { path = "../nym_offline_compact_ecash" }
nym-ecash-time = { path = "../ecash-time" }
nym-network-defaults = { path = "../network-defaults" }
nym-upgrade-mode-check = { path = "../upgrade-mode-check" }
+29
View File
@@ -30,6 +30,35 @@ pub use nym_compact_ecash::{
};
pub use nym_ecash_time::{EcashTime, ecash_today};
pub use nym_network_defaults::TicketTypeRepr;
use nym_network_defaults::TicketTypeRepr::V1MixnetEntry;
/// Default bandwidth amount under which [mixnet] clients will attempt to send additional zk-nyms
/// to increase their allowance.
// currently defined as 20% of entry ticket value
// clients are, of course, free to override this value
pub const DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD: i64 =
(V1MixnetEntry.bandwidth_value() / 5) as i64;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum BandwidthCredential {
ZkNym(Box<CredentialSpendingData>),
UpgradeModeJWT { token: String },
}
impl BandwidthCredential {
pub fn into_zk_nym(self) -> Option<Box<CredentialSpendingData>> {
match self {
BandwidthCredential::ZkNym(credential) => Some(credential),
_ => None,
}
}
}
impl From<CredentialSpendingData> for BandwidthCredential {
fn from(credential: CredentialSpendingData) -> Self {
Self::ZkNym(Box::new(credential))
}
}
#[derive(Debug, Clone)]
pub struct CredentialSigningData {
+3
View File
@@ -51,3 +51,6 @@ anyhow = { workspace = true }
nym-compact-ecash = { path = "../nym_offline_compact_ecash" } # we need specific imports in tests
nym-test-utils = { path = "../test-utils" }
tokio = { workspace = true, features = ["full"] }
[lints]
workspace = true
+53 -13
View File
@@ -19,7 +19,9 @@ pub use shared_key::{
SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey,
};
pub const CURRENT_PROTOCOL_VERSION: u8 = EMBEDDED_KEY_ROTATION_INFO_VERSION;
pub type GatewayProtocolVersion = u8;
pub const CURRENT_PROTOCOL_VERSION: GatewayProtocolVersion = UPGRADE_MODE_VERSION;
/// Defines the current version of the communication protocol between gateway and clients.
/// It has to be incremented for any breaking change.
@@ -29,35 +31,73 @@ pub const CURRENT_PROTOCOL_VERSION: u8 = EMBEDDED_KEY_ROTATION_INFO_VERSION;
// 3 - change to AES-GCM-SIV and non-zero IVs
// 4 - introduction of v2 authentication protocol to prevent reply attacks
// 5 - add key rotation information to the serialised mix packet
pub const INITIAL_PROTOCOL_VERSION: u8 = 1;
pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: u8 = 2;
pub const AES_GCM_SIV_PROTOCOL_VERSION: u8 = 3;
pub const AUTHENTICATE_V2_PROTOCOL_VERSION: u8 = 4;
pub const EMBEDDED_KEY_ROTATION_INFO_VERSION: u8 = 5;
// 6 - support for 'upgrade mode'
pub const INITIAL_PROTOCOL_VERSION: GatewayProtocolVersion = 1;
pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: GatewayProtocolVersion = 2;
pub const AES_GCM_SIV_PROTOCOL_VERSION: GatewayProtocolVersion = 3;
pub const AUTHENTICATE_V2_PROTOCOL_VERSION: GatewayProtocolVersion = 4;
pub const EMBEDDED_KEY_ROTATION_INFO_VERSION: GatewayProtocolVersion = 5;
pub const UPGRADE_MODE_VERSION: GatewayProtocolVersion = 6;
// TODO: could using `Mac` trait here for OutputSize backfire?
// Should hmac itself be exposed, imported and used instead?
pub type LegacyGatewayMacSize = <GatewayIntegrityHmacAlgorithm as OutputSizeUser>::OutputSize;
pub trait GatewayProtocolVersionExt {
const CURRENT: GatewayProtocolVersion = CURRENT_PROTOCOL_VERSION;
fn supports_aes256_gcm_siv(&self) -> bool;
fn supports_authenticate_v2(&self) -> bool;
fn supports_key_rotation_packet(&self) -> bool;
fn supports_upgrade_mode(&self) -> bool;
fn is_future_version(&self) -> bool;
}
impl GatewayProtocolVersionExt for Option<u8> {
impl GatewayProtocolVersionExt for Option<GatewayProtocolVersion> {
fn supports_aes256_gcm_siv(&self) -> bool {
let Some(protocol) = *self else { return false };
protocol >= AES_GCM_SIV_PROTOCOL_VERSION
let Some(protocol) = self else { return false };
protocol.supports_aes256_gcm_siv()
}
fn supports_authenticate_v2(&self) -> bool {
let Some(protocol) = *self else { return false };
protocol >= AUTHENTICATE_V2_PROTOCOL_VERSION
let Some(protocol) = self else { return false };
protocol.supports_authenticate_v2()
}
fn supports_key_rotation_packet(&self) -> bool {
let Some(protocol) = *self else { return false };
protocol >= EMBEDDED_KEY_ROTATION_INFO_VERSION
let Some(protocol) = self else { return false };
protocol.supports_key_rotation_packet()
}
fn supports_upgrade_mode(&self) -> bool {
let Some(protocol) = self else { return false };
protocol.supports_upgrade_mode()
}
fn is_future_version(&self) -> bool {
let Some(protocol) = self else { return false };
protocol.is_future_version()
}
}
impl GatewayProtocolVersionExt for GatewayProtocolVersion {
fn supports_aes256_gcm_siv(&self) -> bool {
*self >= AES_GCM_SIV_PROTOCOL_VERSION
}
fn supports_authenticate_v2(&self) -> bool {
*self >= AUTHENTICATE_V2_PROTOCOL_VERSION
}
fn supports_key_rotation_packet(&self) -> bool {
*self >= EMBEDDED_KEY_ROTATION_INFO_VERSION
}
fn supports_upgrade_mode(&self) -> bool {
*self >= UPGRADE_MODE_VERSION
}
fn is_future_version(&self) -> bool {
*self > CURRENT_PROTOCOL_VERSION
}
}
@@ -3,10 +3,12 @@
use crate::registration::handshake::messages::{Finalization, GatewayMaterialExchange};
use crate::registration::handshake::state::State;
use crate::registration::handshake::SharedGatewayKey;
use crate::registration::handshake::HandshakeResult;
use crate::registration::handshake::{error::HandshakeError, WsItem};
use crate::{GatewayProtocolVersionExt, INITIAL_PROTOCOL_VERSION};
use futures::{Sink, Stream};
use rand::{CryptoRng, RngCore};
use tracing::info;
use tungstenite::Message as WsMessage;
impl<S, R> State<'_, S, R> {
@@ -25,10 +27,26 @@ impl<S, R> State<'_, S, R> {
// 2. wait for response with remote x25519 pubkey as well as encrypted signature
// <- g^y || AES(k, sig(gate_priv, (g^y || g^x)) || MAYBE_NONCE
let mid_res = self
let (mid_res, gateway_protocol) = self
.receive_handshake_message::<GatewayMaterialExchange>()
.await?;
// NEGOTIATE PROTOCOL
if gateway_protocol.is_future_version() {
// SAFETY: future version means it's greater than CURRENT, which is always a `Some`
#[allow(clippy::unwrap_used)]
return Err(HandshakeError::UnsupportedProtocol {
version: gateway_protocol.unwrap(),
});
}
let gateway_protocol = gateway_protocol.unwrap_or(INITIAL_PROTOCOL_VERSION);
// that should never happen, but we're fine with that outcome
if Some(gateway_protocol) != self.proposed_protocol_version() {
info!("the gateway insists on protocol version different from the one we suggested. it wants {gateway_protocol} whilst we wanted {:?}, however, we can support it", self.proposed_protocol_version());
self.set_protocol_version(gateway_protocol);
}
// 3. derive shared keys locally
// hkdf::<blake3>::(g^xy)
self.derive_shared_key(&mid_res.ephemeral_dh, maybe_hkdf_salt.as_deref());
@@ -42,14 +60,14 @@ impl<S, R> State<'_, S, R> {
self.send_handshake_data(materials).await?;
// 6. wait for remote confirmation of finalizing the handshake
let finalization = self.receive_handshake_message::<Finalization>().await?;
let (finalization, _) = self.receive_handshake_message::<Finalization>().await?;
finalization.ensure_success()?;
Ok(())
}
pub(crate) async fn perform_client_handshake(
mut self,
) -> Result<SharedGatewayKey, HandshakeError>
) -> Result<HandshakeResult, HandshakeError>
where
S: Stream<Item = WsItem> + Sink<WsMessage> + Unpin,
R: CryptoRng + RngCore,
@@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
use crate::shared_key::SharedKeyUsageError;
use crate::GatewayProtocolVersion;
use crate::GatewayProtocolVersionExt;
use thiserror::Error;
#[derive(Debug, Error)]
@@ -34,4 +36,10 @@ pub enum HandshakeError {
#[error("timed out waiting for a handshake message")]
Timeout,
#[error("Connection is in an invalid state - please send a bug report")]
ConnectionInInvalidState,
#[error("the gateway requests protocol version that's not supported by this client. it wants to use v{version} whilst we only understand up to v{}", GatewayProtocolVersion::CURRENT)]
UnsupportedProtocol { version: GatewayProtocolVersion },
}
@@ -5,9 +5,11 @@ use crate::registration::handshake::messages::{
HandshakeMessage, Initialisation, MaterialExchange,
};
use crate::registration::handshake::state::State;
use crate::registration::handshake::SharedGatewayKey;
use crate::registration::handshake::HandshakeResult;
use crate::registration::handshake::{error::HandshakeError, WsItem};
use crate::{GatewayProtocolVersion, GatewayProtocolVersionExt};
use futures::{Sink, Stream};
use tracing::{debug, warn};
use tungstenite::Message as WsMessage;
impl<S, R> State<'_, S, R> {
@@ -18,11 +20,39 @@ impl<S, R> State<'_, S, R> {
where
S: Stream<Item = WsItem> + Sink<WsMessage> + Unpin,
{
// NEGOTIATE PROTOCOL
// old clients were sending protocol version as defined by the following:
/*
fn request_protocol_version(&self) -> u8 {
if self.derive_aes256_gcm_siv_key {
AES_GCM_SIV_PROTOCOL_VERSION
} else if self.expects_credential_usage {
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION
} else {
INITIAL_PROTOCOL_VERSION
}
}
*/
// meaning the highest possible value they could have sent was `4` (AUTHENTICATE_V2_PROTOCOL_VERSION)
// so if we received anything higher than that, it means they understand negotiation.
// currently not strictly needed as we just blindly accept what they proposed,
// but will be needed in the future.
if self.proposed_protocol_version().is_future_version() {
// this should never happen in a non-malicious client as it should use at most whatever version this gateway has announced
self.set_protocol_version(GatewayProtocolVersion::CURRENT)
} else {
// currently we accept all protocols, i.e. legacy keys, aes128, etc. so we downgrade to whatever
// the client has proposed. this will change in the future
debug!(
"using the protocol version proposed by the client: {:?}",
self.proposed_protocol_version()
)
}
// 1. receive remote ed25519 pubkey alongside ephemeral x25519 pubkey and maybe a flag indicating non-legacy client
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_NON_LEGACY
let init_message = Initialisation::try_from_bytes(&raw_init_message)?;
self.update_remote_identity(init_message.identity);
self.set_aes256_gcm_siv_key_derivation(!init_message.is_legacy());
// 2. derive shared keys locally
// hkdf::<blake3>::(g^xy)
@@ -39,7 +69,12 @@ impl<S, R> State<'_, S, R> {
self.send_handshake_data(material).await?;
// 4. wait for the remote response with their own encrypted signature
let materials = self.receive_handshake_message::<MaterialExchange>().await?;
let (materials, client_protocol) =
self.receive_handshake_message::<MaterialExchange>().await?;
if client_protocol != self.proposed_protocol_version() {
warn!("the client hasn't accepted our proposed protocol version. we suggested {:?} while it returned {client_protocol:?}", self.proposed_protocol_version());
// TBD what to do here
}
// 5. verify the received signature using the locally derived keys
self.verify_remote_key_material(&materials, &init_message.ephemeral_dh)?;
@@ -54,7 +89,7 @@ impl<S, R> State<'_, S, R> {
pub(crate) async fn perform_gateway_handshake(
mut self,
raw_init_message: Vec<u8>,
) -> Result<SharedGatewayKey, HandshakeError>
) -> Result<HandshakeResult, HandshakeError>
where
S: Stream<Item = WsItem> + Sink<WsMessage> + Unpin,
{
@@ -24,13 +24,6 @@ pub struct Initialisation {
pub initiator_salt: Option<Vec<u8>>,
}
impl Initialisation {
#[cfg(not(target_arch = "wasm32"))]
pub fn is_legacy(&self) -> bool {
self.initiator_salt.is_none()
}
}
#[derive(Debug)]
pub struct MaterialExchange {
pub signature_ciphertext: Vec<u8>,
@@ -99,8 +92,9 @@ impl HandshakeMessage for Initialisation {
let identity = ed25519::PublicKey::from_bytes(&bytes[..ed25519::PUBLIC_KEY_LENGTH])
.map_err(|_| HandshakeError::MalformedRequest)?;
// this can only fail if the provided bytes have len different from encryption::PUBLIC_KEY_SIZE
// SAFETY: this can only fail if the provided bytes have len different from encryption::PUBLIC_KEY_SIZE
// which is impossible
#[allow(clippy::unwrap_used)]
let ephemeral_dh =
x25519::PublicKey::from_bytes(&bytes[ed25519::PUBLIC_KEY_LENGTH..legacy_len]).unwrap();
@@ -194,6 +188,7 @@ impl HandshakeMessage for GatewayMaterialExchange {
// this can only fail if the provided bytes have len different from PUBLIC_KEY_SIZE
// which is impossible
#[allow(clippy::unwrap_used)]
let ephemeral_dh =
x25519::PublicKey::from_bytes(&bytes[..x25519::PUBLIC_KEY_SIZE]).unwrap();
let materials = MaterialExchange::try_from_bytes(&bytes[x25519::PUBLIC_KEY_SIZE..])?;
@@ -3,7 +3,7 @@
use self::error::HandshakeError;
use crate::registration::handshake::state::State;
use crate::SharedGatewayKey;
use crate::{GatewayProtocolVersion, SharedGatewayKey};
use futures::future::BoxFuture;
use futures::{Sink, Stream};
use nym_crypto::asymmetric::ed25519;
@@ -34,24 +34,29 @@ pub const KDF_SALT_LENGTH: usize = 16;
// we do not need to worry about that.
pub struct GatewayHandshake<'a> {
handshake_future: BoxFuture<'a, Result<SharedGatewayKey, HandshakeError>>,
handshake_future: BoxFuture<'a, Result<HandshakeResult, HandshakeError>>,
}
impl Future for GatewayHandshake<'_> {
type Output = Result<SharedGatewayKey, HandshakeError>;
type Output = Result<HandshakeResult, HandshakeError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.handshake_future).poll(cx)
}
}
#[derive(Debug, PartialEq)]
pub struct HandshakeResult {
pub negotiated_protocol: GatewayProtocolVersion,
pub derived_key: SharedGatewayKey,
}
pub fn client_handshake<'a, S, R>(
rng: &'a mut R,
ws_stream: &'a mut S,
identity: &'a ed25519::KeyPair,
gateway_pubkey: ed25519::PublicKey,
expects_credential_usage: bool,
derive_aes256_gcm_siv_key: bool,
gateway_protocol: Option<GatewayProtocolVersion>,
#[cfg(not(target_arch = "wasm32"))] shutdown_token: ShutdownToken,
) -> GatewayHandshake<'a>
where
@@ -63,11 +68,10 @@ where
ws_stream,
identity,
Some(gateway_pubkey),
gateway_protocol,
#[cfg(not(target_arch = "wasm32"))]
shutdown_token,
)
.with_credential_usage(expects_credential_usage)
.with_aes256_gcm_siv_key(derive_aes256_gcm_siv_key);
);
GatewayHandshake {
handshake_future: Box::pin(state.perform_client_handshake()),
@@ -80,13 +84,21 @@ pub fn gateway_handshake<'a, S, R>(
ws_stream: &'a mut S,
identity: &'a ed25519::KeyPair,
received_init_payload: Vec<u8>,
requested_client_protocol: Option<GatewayProtocolVersion>,
shutdown_token: ShutdownToken,
) -> GatewayHandshake<'a>
where
S: Stream<Item = WsItem> + Sink<WsMessage> + Unpin + Send + 'a,
R: CryptoRng + RngCore + Send,
{
let state = State::new(rng, ws_stream, identity, None, shutdown_token);
let state = State::new(
rng,
ws_stream,
identity,
None,
requested_client_protocol,
shutdown_token,
);
GatewayHandshake {
handshake_future: Box::pin(state.perform_gateway_handshake(received_init_payload)),
}
@@ -113,7 +125,8 @@ DONE(status)
#[cfg(test)]
mod tests {
use super::*;
use crate::ClientControlRequest;
use crate::{ClientControlRequest, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION};
use anyhow::{bail, Context};
use futures::StreamExt;
use nym_test_utils::helpers::u64_seeded_rng;
use nym_test_utils::mocks::stream_sink::mock_streams;
@@ -121,10 +134,53 @@ mod tests {
use tokio::join;
use tungstenite::Message;
#[tokio::test]
async fn basic_handshake() -> anyhow::Result<()> {
use anyhow::Context as _;
trait ClientControlRequestExt {
async fn get_handshake_init_data(&mut self) -> anyhow::Result<Vec<u8>> {
let ClientControlRequest::RegisterHandshakeInitRequest {
protocol_version: _,
data,
} = self.get_control_request().await?
else {
bail!("unexpected ClientControlRequest")
};
Ok(data)
}
async fn get_control_request(&mut self) -> anyhow::Result<ClientControlRequest>;
}
impl<T> ClientControlRequestExt for T
where
T: Stream<Item = WsItem> + Unpin,
{
async fn get_control_request(&mut self) -> anyhow::Result<ClientControlRequest> {
let msg = self
.next()
.timeboxed()
.await
.context("timeout")?
.context("no message!")??
.into_text()?
.parse::<ClientControlRequest>()?;
Ok(msg)
}
}
struct Party<R: 'static, S: 'static> {
rng: &'static mut R,
keys: &'static mut ed25519::KeyPair,
socket: &'static mut S,
}
fn setup() -> (
Party<
impl CryptoRng + RngCore + Send,
impl Stream<Item = WsItem> + Sink<WsMessage> + Unpin,
>,
Party<
impl CryptoRng + RngCore + Send,
impl Stream<Item = WsItem> + Sink<WsMessage> + Unpin,
>,
) {
// solve the lifetime issue by just leaking the contents of the boxes
// which is perfectly fine in test
let client_rng = u64_seeded_rng(42).leak();
@@ -142,51 +198,139 @@ mod tests {
let client_ws = client_ws.leak();
let gateway_ws = gateway_ws.leak();
(
Party {
rng: client_rng,
keys: client_keys,
socket: client_ws,
},
Party {
rng: gateway_rng,
keys: gateway_keys,
socket: gateway_ws,
},
)
}
#[tokio::test]
async fn basic_handshake() -> anyhow::Result<()> {
let (client, gateway) = setup();
let handshake_client = client_handshake(
client_rng,
client_ws,
client_keys,
*gateway_keys.public_key(),
false,
true,
client.rng,
client.socket,
client.keys,
*gateway.keys.public_key(),
Some(CURRENT_PROTOCOL_VERSION),
ShutdownToken::default(),
);
let client_fut = handshake_client.spawn_timeboxed();
// we need to receive the first message so that it could be propagated to the gateway side of the handshake
let ClientControlRequest::RegisterHandshakeInitRequest {
protocol_version: _,
data,
} = (gateway_ws.next())
.timeboxed()
.await
.context("timeout")?
.context("no message!")??
.into_text()?
.parse::<ClientControlRequest>()?
else {
panic!("bad message")
};
let init_msg = data;
let init_msg = gateway.socket.get_handshake_init_data().await?;
let handshake_gateway = gateway_handshake(
gateway_rng,
gateway_ws,
gateway_keys,
gateway.rng,
gateway.socket,
gateway.keys,
init_msg,
Some(CURRENT_PROTOCOL_VERSION),
ShutdownToken::default(),
);
let gateway_fut = handshake_gateway.spawn_timeboxed();
let (client, gateway) = join!(client_fut, gateway_fut);
let client_key = client???;
let gateway_key = gateway???;
let client_res = client???;
let gateway_res = gateway???;
// ensure the created keys are the same
assert_eq!(client_key, gateway_key);
assert_eq!(client_res, gateway_res);
assert_eq!(client_res.negotiated_protocol, CURRENT_PROTOCOL_VERSION);
Ok(())
}
#[tokio::test]
async fn protocol_downgrade() -> anyhow::Result<()> {
let (client, gateway) = setup();
let handshake_client = client_handshake(
client.rng,
client.socket,
client.keys,
*gateway.keys.public_key(),
Some(CURRENT_PROTOCOL_VERSION + 42),
ShutdownToken::default(),
);
let client_fut = handshake_client.spawn_timeboxed();
// we need to receive the first message so that it could be propagated to the gateway side of the handshake
let init_msg = gateway.socket.get_handshake_init_data().await?;
let handshake_gateway = gateway_handshake(
gateway.rng,
gateway.socket,
gateway.keys,
init_msg,
Some(CURRENT_PROTOCOL_VERSION + 42),
ShutdownToken::default(),
);
let gateway_fut = handshake_gateway.spawn_timeboxed();
let (client, gateway) = join!(client_fut, gateway_fut);
let client_res = client???;
let gateway_res = gateway???;
// ensure the created keys are the same
assert_eq!(client_res, gateway_res);
// and the protocol got downgraded for both parties
assert_eq!(client_res.negotiated_protocol, CURRENT_PROTOCOL_VERSION);
Ok(())
}
#[tokio::test]
async fn protocol_upgrade() -> anyhow::Result<()> {
let (client, gateway) = setup();
let handshake_client = client_handshake(
client.rng,
client.socket,
client.keys,
*gateway.keys.public_key(),
None,
ShutdownToken::default(),
);
let client_fut = handshake_client.spawn_timeboxed();
// we need to receive the first message so that it could be propagated to the gateway side of the handshake
let init_msg = gateway.socket.get_handshake_init_data().await?;
let handshake_gateway = gateway_handshake(
gateway.rng,
gateway.socket,
gateway.keys,
init_msg,
None,
ShutdownToken::default(),
);
let gateway_fut = handshake_gateway.spawn_timeboxed();
let (client, gateway) = join!(client_fut, gateway_fut);
let client_res = client???;
let gateway_res = gateway???;
// ensure the created keys are the same
assert_eq!(client_res, gateway_res);
// and the protocol got upgraded to the first known version
assert_eq!(client_res.negotiated_protocol, INITIAL_PROTOCOL_VERSION);
Ok(())
}
@@ -5,11 +5,11 @@ use crate::registration::handshake::error::HandshakeError;
use crate::registration::handshake::messages::{
HandshakeMessage, Initialisation, MaterialExchange,
};
use crate::registration::handshake::{SharedGatewayKey, WsItem, KDF_SALT_LENGTH};
use crate::registration::handshake::{HandshakeResult, SharedGatewayKey, WsItem, KDF_SALT_LENGTH};
use crate::shared_key::SharedKeySize;
use crate::{
types, LegacySharedKeySize, LegacySharedKeys, SharedSymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION,
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION,
types, GatewayProtocolVersion, GatewayProtocolVersionExt, LegacySharedKeySize,
LegacySharedKeys, SharedSymmetricKey, INITIAL_PROTOCOL_VERSION,
};
use futures::{Sink, SinkExt, Stream, StreamExt};
use nym_crypto::asymmetric::{ed25519, x25519};
@@ -54,12 +54,11 @@ pub(crate) struct State<'a, S, R> {
/// Ideally it would always be known before the handshake was initiated.
remote_pubkey: Option<ed25519::PublicKey>,
// this field is really out of place here, however, we need to propagate this information somehow
// in order to establish correct protocol for backwards compatibility reasons
expects_credential_usage: bool,
/// Specifies whether the end product should be an AES128Ctr + blake3 HMAC keys (legacy) or AES256-GCM-SIV (current)
derive_aes256_gcm_siv_key: bool,
/// Version of the protocol to use during the handshake that also implicitly specifies
/// additional features such as the type of derived shared keys, i.e.
/// AES128Ctr + blake3 HMAC keys (legacy) or AES256-GCM-SIV (current)
/// the above is decided by whether the specified protocol version supports the new variant or not.
protocol_version: Option<GatewayProtocolVersion>,
// channel to receive shutdown signal
#[cfg(not(target_arch = "wasm32"))]
@@ -72,6 +71,7 @@ impl<'a, S, R> State<'a, S, R> {
ws_stream: &'a mut S,
identity: &'a ed25519::KeyPair,
remote_pubkey: Option<ed25519::PublicKey>,
protocol_version: Option<GatewayProtocolVersion>,
#[cfg(not(target_arch = "wasm32"))] shutdown_token: ShutdownToken,
) -> Self
where
@@ -84,40 +84,31 @@ impl<'a, S, R> State<'a, S, R> {
ephemeral_keypair,
identity,
remote_pubkey,
protocol_version,
derived_shared_keys: None,
// later on this should become the default
expects_credential_usage: false,
derive_aes256_gcm_siv_key: false,
#[cfg(not(target_arch = "wasm32"))]
shutdown_token,
}
}
pub(crate) fn with_credential_usage(mut self, expects_credential_usage: bool) -> Self {
self.expects_credential_usage = expects_credential_usage;
self
}
pub(crate) fn with_aes256_gcm_siv_key(mut self, derive_aes256_gcm_siv_key: bool) -> Self {
self.derive_aes256_gcm_siv_key = derive_aes256_gcm_siv_key;
self
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn set_aes256_gcm_siv_key_derivation(&mut self, derive_aes256_gcm_siv_key: bool) {
self.derive_aes256_gcm_siv_key = derive_aes256_gcm_siv_key;
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn local_ephemeral_key(&self) -> &x25519::PublicKey {
self.ephemeral_keypair.public_key()
}
pub(crate) fn proposed_protocol_version(&self) -> Option<GatewayProtocolVersion> {
self.protocol_version
}
pub(crate) fn set_protocol_version(&mut self, protocol_version: GatewayProtocolVersion) {
self.protocol_version = Some(protocol_version);
}
pub(crate) fn maybe_generate_initiator_salt(&mut self) -> Option<Vec<u8>>
where
R: CryptoRng + RngCore,
{
if self.derive_aes256_gcm_siv_key {
if self.protocol_version.supports_aes256_gcm_siv() {
let mut salt = vec![0u8; KDF_SALT_LENGTH];
self.rng.fill_bytes(&mut salt);
Some(salt)
@@ -154,13 +145,14 @@ impl<'a, S, R> State<'a, S, R> {
.private_key()
.diffie_hellman(remote_ephemeral_key);
let key_size = if self.derive_aes256_gcm_siv_key {
let key_size = if self.protocol_version.supports_aes256_gcm_siv() {
SharedKeySize::to_usize()
} else {
LegacySharedKeySize::to_usize()
};
// there is no reason for this to fail as our okm is expected to be only 16 bytes
// SAFETY: there is no reason for this to fail as our okm is expected to be only 16 bytes
#[allow(clippy::expect_used)]
let okm = hkdf::extract_then_expand::<GatewaySharedKeyHkdfAlgorithm>(
initiator_salt,
&dh_result,
@@ -169,11 +161,14 @@ impl<'a, S, R> State<'a, S, R> {
)
.expect("somehow too long okm was provided");
let shared_key = if self.derive_aes256_gcm_siv_key {
// SAFETY: the okm has been expanded to the length expected by the corresponding keys
let shared_key = if self.protocol_version.supports_aes256_gcm_siv() {
#[allow(clippy::expect_used)]
let current_key = SharedSymmetricKey::try_from_bytes(&okm)
.expect("okm was expanded to incorrect length!");
SharedGatewayKey::Current(current_key)
} else {
#[allow(clippy::expect_used)]
let legacy_key = LegacySharedKeys::try_from_bytes(&okm)
.expect("okm was expanded to incorrect length!");
SharedGatewayKey::Legacy(legacy_key)
@@ -196,7 +191,7 @@ impl<'a, S, R> State<'a, S, R> {
.collect();
let signature = self.identity.private_key().sign(plaintext);
let nonce = if self.derive_aes256_gcm_siv_key {
let nonce = if self.protocol_version.supports_aes256_gcm_siv() {
let mut rng = thread_rng();
Some(random_nonce::<GatewayEncryptionAlgorithm, _>(&mut rng).to_vec())
} else {
@@ -204,6 +199,7 @@ impl<'a, S, R> State<'a, S, R> {
};
// SAFETY: this function is only called after the local key has already been derived
#[allow(clippy::expect_used)]
let signature_ciphertext = self
.derived_shared_keys
.as_ref()
@@ -222,13 +218,14 @@ impl<'a, S, R> State<'a, S, R> {
remote_ephemeral_key: &x25519::PublicKey,
) -> Result<(), HandshakeError> {
// SAFETY: this function is only called after the local key has already been derived
#[allow(clippy::expect_used)]
let derived_shared_key = self
.derived_shared_keys
.as_ref()
.expect("shared key was not derived!");
// if the [client] init message contained non-legacy flag, the associated nonce MUST be present
if self.derive_aes256_gcm_siv_key && remote_response.nonce.is_none() {
if self.protocol_version.supports_aes256_gcm_siv() && remote_response.nonce.is_none() {
return Err(HandshakeError::MissingNonceForCurrentKey);
}
@@ -249,6 +246,7 @@ impl<'a, S, R> State<'a, S, R> {
.chain(self.ephemeral_keypair.public_key().to_bytes())
.collect();
#[allow(clippy::unwrap_used)]
self.remote_pubkey
.as_ref()
.unwrap()
@@ -261,7 +259,10 @@ impl<'a, S, R> State<'a, S, R> {
self.remote_pubkey = Some(remote_pubkey)
}
fn on_wg_msg(msg: Option<WsItem>) -> Result<Option<Vec<u8>>, HandshakeError> {
#[allow(clippy::complexity)]
fn on_wg_msg(
msg: Option<WsItem>,
) -> Result<Option<(Vec<u8>, Option<GatewayProtocolVersion>)>, HandshakeError> {
let Some(msg) = msg else {
return Err(HandshakeError::ClosedStream);
};
@@ -277,9 +278,10 @@ impl<'a, S, R> State<'a, S, R> {
// hehe, that's a bit disgusting that the type system requires we explicitly ignore the
// protocol_version field that we actually never attach at this point
// yet another reason for the overdue refactor
types::RegistrationHandshake::HandshakePayload { data, .. } => {
Ok(Some(data))
}
types::RegistrationHandshake::HandshakePayload {
protocol_version,
data,
} => Ok(Some((data, protocol_version))),
types::RegistrationHandshake::HandshakeError { message } => {
Err(HandshakeError::RemoteError(message))
}
@@ -299,7 +301,9 @@ impl<'a, S, R> State<'a, S, R> {
}
#[cfg(not(target_arch = "wasm32"))]
async fn _receive_handshake_message_bytes(&mut self) -> Result<Vec<u8>, HandshakeError>
async fn _receive_handshake_message_bytes(
&mut self,
) -> Result<(Vec<u8>, Option<GatewayProtocolVersion>), HandshakeError>
where
S: Stream<Item = WsItem> + Unpin,
{
@@ -318,7 +322,9 @@ impl<'a, S, R> State<'a, S, R> {
}
#[cfg(target_arch = "wasm32")]
async fn _receive_handshake_message_bytes(&mut self) -> Result<Vec<u8>, HandshakeError>
async fn _receive_handshake_message_bytes(
&mut self,
) -> Result<(Vec<u8>, Option<GatewayProtocolVersion>), HandshakeError>
where
S: Stream<Item = WsItem> + Unpin,
{
@@ -331,20 +337,22 @@ impl<'a, S, R> State<'a, S, R> {
}
}
pub(crate) async fn receive_handshake_message<M>(&mut self) -> Result<M, HandshakeError>
pub(crate) async fn receive_handshake_message<M>(
&mut self,
) -> Result<(M, Option<GatewayProtocolVersion>), HandshakeError>
where
S: Stream<Item = WsItem> + Unpin,
M: HandshakeMessage,
{
// TODO: make timeout duration configurable
let bytes = timeout(
let (bytes, protocol) = timeout(
Duration::from_secs(5),
self._receive_handshake_message_bytes(),
)
.await
.map_err(|_| HandshakeError::Timeout)??;
M::try_from_bytes(&bytes)
M::try_from_bytes(&bytes).map(|msg| (msg, protocol))
}
// upon receiving this, the receiver should terminate the handshake
@@ -357,21 +365,11 @@ impl<'a, S, R> State<'a, S, R> {
{
let handshake_message = types::RegistrationHandshake::new_error(message);
self.ws_stream
.send(WsMessage::Text(handshake_message.try_into().unwrap()))
.send(WsMessage::Text(handshake_message.into()))
.await
.map_err(|_| HandshakeError::ClosedStream)
}
fn request_protocol_version(&self) -> u8 {
if self.derive_aes256_gcm_siv_key {
AES_GCM_SIV_PROTOCOL_VERSION
} else if self.expects_credential_usage {
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION
} else {
INITIAL_PROTOCOL_VERSION
}
}
pub(crate) async fn send_handshake_data<M>(
&mut self,
inner_message: M,
@@ -384,18 +382,25 @@ impl<'a, S, R> State<'a, S, R> {
let handshake_message = types::RegistrationHandshake::new_payload(
inner_message.into_bytes(),
self.request_protocol_version(),
self.protocol_version,
);
self.ws_stream
.send(WsMessage::Text(handshake_message.try_into().unwrap()))
.send(WsMessage::Text(handshake_message.into()))
.await
.map_err(|_| HandshakeError::ClosedStream)
}
/// Finish the handshake, yielding the derived shared key and implicitly dropping all borrowed
/// values.
pub(crate) fn finalize_handshake(self) -> SharedGatewayKey {
self.derived_shared_keys.unwrap()
pub(crate) fn finalize_handshake(self) -> HandshakeResult {
// SAFETY: handshake can't be finalised without deriving the shared keys
#[allow(clippy::unwrap_used)]
HandshakeResult {
negotiated_protocol: self
.proposed_protocol_version()
.unwrap_or(INITIAL_PROTOCOL_VERSION),
derived_key: self.derived_shared_keys.unwrap(),
}
}
// If any step along the way failed (that are non-network related),
@@ -43,6 +43,7 @@ impl LegacySharedKeys {
rng.fill_bytes(&mut salt);
let legacy_bytes = Zeroizing::new(self.to_bytes());
#[allow(clippy::expect_used)]
let okm = hkdf::extract_then_expand::<GatewaySharedKeyHkdfAlgorithm>(
Some(&salt),
&legacy_bytes,
@@ -51,6 +52,7 @@ impl LegacySharedKeys {
)
.expect("somehow too long okm was provided");
#[allow(clippy::expect_used)]
let key = SharedSymmetricKey::try_from_bytes(&okm)
.expect("okm was expanded to incorrect length!");
(key, salt)
@@ -62,6 +64,7 @@ impl LegacySharedKeys {
expected_digest: &[u8],
) -> Option<SharedSymmetricKey> {
let legacy_bytes = Zeroizing::new(self.to_bytes());
#[allow(clippy::expect_used)]
let okm = hkdf::extract_then_expand::<GatewaySharedKeyHkdfAlgorithm>(
Some(salt),
&legacy_bytes,
@@ -69,6 +72,8 @@ impl LegacySharedKeys {
SharedKeySize::to_usize(),
)
.expect("somehow too long okm was provided");
#[allow(clippy::expect_used)]
let key = SharedSymmetricKey::try_from_bytes(&okm)
.expect("okm was expanded to incorrect length!");
if key.digest() != expected_digest {
@@ -47,6 +47,8 @@ impl SharedGatewayKey {
}
}
// it is responsibility of the caller to ensure the correct variant is present
#[allow(clippy::panic)]
pub fn unwrap_legacy(&self) -> &LegacySharedKeys {
match self {
SharedGatewayKey::Current(_) => panic!("expected legacy key"),
+1 -1
View File
@@ -13,7 +13,7 @@ use thiserror::Error;
use time::OffsetDateTime;
// specific errors (that should not be nested!!) for clients to match on
#[derive(Debug, Copy, Clone, Error, Serialize, Deserialize)]
#[derive(Debug, Copy, Clone, Error, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum SimpleGatewayRequestsError {
#[error("insufficient bandwidth available to process the request. required: {required}B, available: {available}B")]
@@ -1,6 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::GatewayProtocolVersion;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
@@ -9,7 +10,7 @@ use std::str::FromStr;
pub enum RegistrationHandshake {
HandshakePayload {
#[serde(default)]
protocol_version: Option<u8>,
protocol_version: Option<GatewayProtocolVersion>,
data: Vec<u8>,
},
HandshakeError {
@@ -18,9 +19,9 @@ pub enum RegistrationHandshake {
}
impl RegistrationHandshake {
pub fn new_payload(data: Vec<u8>, protocol_version: u8) -> Self {
pub fn new_payload(data: Vec<u8>, protocol_version: Option<GatewayProtocolVersion>) -> Self {
RegistrationHandshake::HandshakePayload {
protocol_version: Some(protocol_version),
protocol_version,
data,
}
}
@@ -48,11 +49,11 @@ impl TryFrom<String> for RegistrationHandshake {
}
}
impl TryInto<String> for RegistrationHandshake {
type Error = serde_json::Error;
fn try_into(self) -> Result<String, serde_json::Error> {
serde_json::to_string(&self)
impl From<RegistrationHandshake> for String {
fn from(value: RegistrationHandshake) -> Self {
// SAFETY: we have infallible serde implementation
#[allow(clippy::unwrap_used)]
serde_json::to_string(&value).unwrap()
}
}
@@ -79,7 +80,7 @@ mod tests {
assert_eq!(protocol_version, Some(42));
assert_eq!(data, handshake_data)
}
_ => unreachable!("this branch shouldn't have been reached!"),
_ => panic!("this branch shouldn't have been reached!"),
}
let handshake_payload_without_protocol = RegistrationHandshake::HandshakePayload {
@@ -97,7 +98,7 @@ mod tests {
assert!(protocol_version.is_none());
assert_eq!(data, handshake_data)
}
_ => unreachable!("this branch shouldn't have been reached!"),
_ => panic!("this branch shouldn't have been reached!"),
}
}
}
@@ -1,7 +1,9 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::{AuthenticationFailure, GatewayRequestsError, SharedGatewayKey};
use crate::{
AuthenticationFailure, GatewayProtocolVersion, GatewayRequestsError, SharedGatewayKey,
};
use nym_crypto::asymmetric::ed25519;
use serde::{Deserialize, Serialize};
use std::iter;
@@ -20,7 +22,7 @@ pub struct AuthenticateRequest {
impl AuthenticateRequest {
pub fn new(
protocol_version: u8,
protocol_version: GatewayProtocolVersion,
shared_key: &SharedGatewayKey,
identity_keys: &ed25519::KeyPair,
) -> Result<AuthenticateRequest, GatewayRequestsError> {
@@ -98,7 +100,7 @@ impl AuthenticateRequest {
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct AuthenticateRequestContent {
pub protocol_version: u8,
pub protocol_version: GatewayProtocolVersion,
// this is identical to the client's address
pub client_identity: ed25519::PublicKey,
@@ -4,9 +4,8 @@
use crate::models::CredentialSpendingRequest;
use crate::text_request::authenticate::AuthenticateRequest;
use crate::{
GatewayRequestsError, SharedGatewayKey, SymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION,
AUTHENTICATE_V2_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION,
INITIAL_PROTOCOL_VERSION,
GatewayProtocolVersion, GatewayRequestsError, SharedGatewayKey, SymmetricKey,
AES_GCM_SIV_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION,
};
use nym_credentials_interface::CredentialSpendingData;
use nym_crypto::asymmetric::ed25519;
@@ -46,6 +45,7 @@ impl ClientRequest {
// - the schema is self-describing which simplifies deserialisation
// SAFETY: the trait has been derived correctly with no weird variants
#[allow(clippy::unwrap_used)]
let plaintext = serde_json::to_vec(self).unwrap();
let nonce = key.random_nonce_or_iv();
let ciphertext = key.encrypt(&plaintext, Some(&nonce))?;
@@ -72,7 +72,7 @@ pub enum ClientControlRequest {
// have the shared key derived?
Authenticate {
#[serde(default)]
protocol_version: Option<u8>,
protocol_version: Option<GatewayProtocolVersion>,
address: String,
enc_address: String,
iv: String,
@@ -83,7 +83,7 @@ pub enum ClientControlRequest {
#[serde(alias = "handshakePayload")]
RegisterHandshakeInitRequest {
#[serde(default)]
protocol_version: Option<u8>,
protocol_version: Option<GatewayProtocolVersion>,
data: Vec<u8>,
},
BandwidthCredential {
@@ -98,6 +98,10 @@ pub enum ClientControlRequest {
enc_credential: Vec<u8>,
iv: Vec<u8>,
},
UpgradeModeJWT {
// no need to encrypt it as it's public anyway
token: String,
},
ClaimFreeTestnetBandwidth,
EncryptedRequest {
ciphertext: Vec<u8>,
@@ -108,12 +112,14 @@ pub enum ClientControlRequest {
}
impl ClientControlRequest {
pub fn new_authenticate(
pub fn new_legacy_authenticate(
address: DestinationAddressBytes,
shared_key: &SharedGatewayKey,
uses_credentials: bool,
) -> Result<Self, GatewayRequestsError> {
// if we're encrypting with non-legacy key, the remote must support AES256-GCM-SIV
// since we are using legacy authentication, the gateway definitely doesn't understand the protocol downgrade,
// so use the lowest possible version we can
let protocol_version = if !shared_key.is_legacy() {
Some(AES_GCM_SIV_PROTOCOL_VERSION)
} else if uses_credentials {
@@ -138,10 +144,8 @@ impl ClientControlRequest {
pub fn new_authenticate_v2(
shared_key: &SharedGatewayKey,
identity_keys: &ed25519::KeyPair,
protocol_version: GatewayProtocolVersion,
) -> Result<Self, GatewayRequestsError> {
// if we're using v2 authentication, we must announce at least that protocol version
let protocol_version = AUTHENTICATE_V2_PROTOCOL_VERSION;
Ok(ClientControlRequest::AuthenticateV2(Box::new(
AuthenticateRequest::new(protocol_version, shared_key, identity_keys)?,
)))
@@ -159,6 +163,7 @@ impl ClientControlRequest {
"BandwidthCredentialV2".to_string()
}
ClientControlRequest::EcashCredential { .. } => "EcashCredential".to_string(),
ClientControlRequest::UpgradeModeJWT { .. } => "UpgradeModeJWT".to_string(),
ClientControlRequest::ClaimFreeTestnetBandwidth => {
"ClaimFreeTestnetBandwidth".to_string()
}
@@ -192,12 +197,16 @@ impl ClientControlRequest {
CredentialSpendingRequest::try_from_bytes(credential_bytes.as_slice())
.map_err(|_| GatewayRequestsError::MalformedEncryption)
}
pub fn new_upgrade_mode_jwt(token: String) -> Self {
ClientControlRequest::UpgradeModeJWT { token }
}
}
impl From<ClientControlRequest> for Message {
fn from(req: ClientControlRequest) -> Self {
// it should be safe to call `unwrap` here as the message is generated by the server
// so if it fails (and consequently panics) it's a bug that should be resolved
// SAFETY: all of the enum variants have valid (for json) serde impl
#[allow(clippy::unwrap_used)]
let str_req = serde_json::to_string(&req).unwrap();
Message::Text(str_req)
}
@@ -1,7 +1,9 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{GatewayRequestsError, SimpleGatewayRequestsError, SymmetricKey};
use crate::{
GatewayProtocolVersion, GatewayRequestsError, SimpleGatewayRequestsError, SymmetricKey,
};
use serde::{Deserialize, Serialize};
use tungstenite::Message;
@@ -26,6 +28,7 @@ impl SensitiveServerResponse {
// - the schema is self-describing which simplifies deserialisation
// SAFETY: the trait has been derived correctly with no weird variants
#[allow(clippy::unwrap_used)]
let plaintext = serde_json::to_vec(self).unwrap();
let nonce = key.random_nonce_or_iv();
let ciphertext = key.encrypt(&plaintext, Some(&nonce))?;
@@ -43,31 +46,57 @@ impl SensitiveServerResponse {
}
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct BandwidthResponse {
pub available_total: i64,
/// Flag indicating whether the gateway has detected the system is undergoing the upgrade
/// (thus it will not meter bandwidth)
#[serde(default)]
pub upgrade_mode: bool,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct SendResponse {
pub remaining_bandwidth: i64,
/// Flag indicating whether the gateway has detected the system is undergoing the upgrade
/// (thus it will not meter bandwidth)
#[serde(default)]
pub upgrade_mode: bool,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(tag = "type", rename_all = "camelCase")]
#[non_exhaustive]
pub enum ServerResponse {
Authenticate {
#[serde(default)]
protocol_version: Option<u8>,
protocol_version: Option<GatewayProtocolVersion>,
status: bool,
bandwidth_remaining: i64,
/// Flag indicating whether the gateway has detected the system is undergoing the upgrade
/// (thus it will not meter bandwidth)
#[serde(default)]
upgrade_mode: bool,
},
Register {
#[serde(default)]
protocol_version: Option<u8>,
protocol_version: Option<GatewayProtocolVersion>,
status: bool,
/// Flag indicating whether the gateway has detected the system is undergoing the upgrade
/// (thus it will not meter bandwidth)
#[serde(default)]
upgrade_mode: bool,
},
EncryptedResponse {
ciphertext: Vec<u8>,
nonce: Vec<u8>,
},
Bandwidth {
available_total: i64,
},
Send {
remaining_bandwidth: i64,
},
Bandwidth(BandwidthResponse),
Send(SendResponse),
SupportedProtocol {
version: u8,
},
@@ -122,6 +151,7 @@ impl From<ServerResponse> for Message {
fn from(res: ServerResponse) -> Self {
// it should be safe to call `unwrap` here as the message is generated by the server
// so if it fails (and consequently panics) it's a bug that should be resolved
#[allow(clippy::unwrap_used)]
let str_res = serde_json::to_string(&res).unwrap();
Message::Text(str_res)
}
@@ -134,3 +164,79 @@ impl TryFrom<String> for ServerResponse {
serde_json::from_str(&msg)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn server_response_serde_compat() {
// make sure new serialisation is identical and compatible
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(tag = "type", rename_all = "camelCase")]
#[non_exhaustive]
pub enum OldServerResponse {
Bandwidth { available_total: i64 },
Send { remaining_bandwidth: i64 },
}
// OLD => NEW
let old_bandwidth = OldServerResponse::Bandwidth {
available_total: 100,
};
let old_send = OldServerResponse::Send {
remaining_bandwidth: 100,
};
let old_bandwidth_str = serde_json::to_string(&old_bandwidth).unwrap();
let old_send_str = serde_json::to_string(&old_send).unwrap();
let recovered_bandwidth = ServerResponse::try_from(old_bandwidth_str).unwrap();
assert_eq!(
recovered_bandwidth,
ServerResponse::Bandwidth(BandwidthResponse {
available_total: 100,
upgrade_mode: false
})
);
let recovered_send = ServerResponse::try_from(old_send_str).unwrap();
assert_eq!(
recovered_send,
ServerResponse::Send(SendResponse {
remaining_bandwidth: 100,
upgrade_mode: false
})
);
// NEW => OLD
let new_bandwidth = ServerResponse::Bandwidth(BandwidthResponse {
available_total: 100,
upgrade_mode: false,
});
let new_send = ServerResponse::Send(SendResponse {
remaining_bandwidth: 100,
upgrade_mode: false,
});
let new_bandwidth_str = serde_json::to_string(&new_bandwidth).unwrap();
let new_send_str = serde_json::to_string(&new_send).unwrap();
let recovered_bandwidth: OldServerResponse =
serde_json::from_str(&new_bandwidth_str).unwrap();
assert_eq!(
recovered_bandwidth,
OldServerResponse::Bandwidth {
available_total: 100
}
);
let recovered_send: OldServerResponse = serde_json::from_str(&new_send_str).unwrap();
assert_eq!(
recovered_send,
OldServerResponse::Send {
remaining_bandwidth: 100
}
);
}
}
+2 -3
View File
@@ -32,7 +32,7 @@ thiserror = { workspace = true }
tracing = { workspace = true }
itertools = { workspace = true }
inventory = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros", "time"] }
# used for decoding text responses (they were already implicitly included)
bytes = { workspace = true }
encoding_rs = { workspace = true }
@@ -52,5 +52,4 @@ workspace = true
features = ["tokio"]
[dev-dependencies]
tokio = { workspace = true, features = ["rt", "macros"] }
tracing-subscriber.workspace = true
+315 -88
View File
@@ -30,19 +30,26 @@
use crate::ClientBuilder;
use std::{
net::SocketAddr,
collections::HashMap,
net::{IpAddr, SocketAddr},
str::FromStr,
sync::{Arc, LazyLock},
time::Duration,
};
use hickory_resolver::{
ResolveError, TokioResolver,
config::{LookupIpStrategy, NameServerConfigGroup, ResolverConfig, ServerOrderingStrategy},
lookup_ip::{LookupIp, LookupIpIntoIter},
TokioResolver,
config::{LookupIpStrategy, NameServerConfigGroup, ResolverConfig},
lookup_ip::LookupIpIntoIter,
name_server::TokioConnectionProvider,
};
use once_cell::sync::OnceCell;
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
use tracing::warn;
use tracing::*;
mod constants;
mod static_resolver;
pub use static_resolver::*;
impl ClientBuilder {
/// Override the DNS resolver implementation used by the underlying http client.
@@ -59,10 +66,6 @@ impl ClientBuilder {
}
}
struct SocketAddrs {
iter: LookupIpIntoIter,
}
// n.b. static items do not call [`Drop`] on program termination, so this won't be deallocated.
// this is fine, as the OS can deallocate the terminated program faster than we can free memory
// but tools like valgrind might report "memory leaks" as it isn't obvious this is intentional.
@@ -72,11 +75,24 @@ static SHARED_RESOLVER: LazyLock<HickoryDnsResolver> = LazyLock::new(|| {
});
#[derive(Debug, thiserror::Error)]
#[error("hickory-dns resolver error: {hickory_error}")]
#[allow(missing_docs)]
/// Error occurring while resolving a hostname into an IP address.
pub struct HickoryDnsError {
#[from]
hickory_error: ResolveError,
pub enum ResolveError {
#[error("invalid name: {0}")]
InvalidNameError(String),
#[error("hickory-dns resolver error: {0}")]
ResolveError(#[from] hickory_resolver::ResolveError),
#[error("high level lookup timed out")]
Timeout,
#[error("hostname not found in static lookup table")]
StaticLookupMiss,
}
impl ResolveError {
/// Returns true if the error is a timeout.
pub fn is_timeout(&self) -> bool {
matches!(self, ResolveError::Timeout)
}
}
/// Wrapper around an `AsyncResolver`, which implements the `Resolve` trait.
@@ -87,69 +103,116 @@ pub struct HickoryDnsError {
/// The default initialization uses a shared underlying `AsyncResolver`. If a thread local resolver
/// is required use `thread_resolver()` to build a resolver with an independently instantiated
/// internal `AsyncResolver`.
#[derive(Debug, Default, Clone)]
#[derive(Debug, Clone)]
pub struct HickoryDnsResolver {
// Since we might not have been called in the context of a
// Tokio Runtime in initialization, so we must delay the actual
// construction of the resolver.
state: Arc<OnceCell<TokioResolver>>,
fallback: Option<Arc<OnceCell<TokioResolver>>>,
static_base: Option<Arc<OnceCell<StaticResolver>>>,
dont_use_shared: bool,
/// Overall timeout for dns lookup associated with any individual host resolution. For example,
/// use of retries, server_ordering_strategy, etc. ends absolutely if this timeout is reached.
overall_dns_timeout: Duration,
}
impl Default for HickoryDnsResolver {
fn default() -> Self {
Self {
state: Default::default(),
fallback: Default::default(),
static_base: Default::default(),
dont_use_shared: Default::default(),
overall_dns_timeout: Duration::from_secs(10),
}
}
}
impl Resolve for HickoryDnsResolver {
fn resolve(&self, name: Name) -> Resolving {
let resolver = self.state.clone();
let maybe_fallback = self.fallback.clone();
let maybe_static = self.static_base.clone();
let independent = self.dont_use_shared;
let overall_dns_timeout = self.overall_dns_timeout;
Box::pin(async move {
let resolver = resolver.get_or_try_init(|| {
// using a closure here is slightly gross, but this makes sure that if the
// lazy-init returns an error it can be handled by the client
if independent {
new_resolver()
} else {
Ok(SHARED_RESOLVER.state.get_or_try_init(new_resolver)?.clone())
}
})?;
resolve(
name,
resolver,
maybe_fallback,
maybe_static,
independent,
overall_dns_timeout,
)
.await
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
})
}
}
// try the primary DNS resolver that we set up (DoH or DoT or whatever)
let lookup = match resolver.lookup_ip(name.as_str()).await {
Ok(res) => res,
Err(e) => {
if let Some(ref fallback) = maybe_fallback {
// on failure use the fall back system configured DNS resolver
if !e.is_no_records_found() {
warn!("primary DNS failed w/ error {e}: using system fallback");
}
let resolver = fallback.get_or_try_init(|| {
// using a closure here is slightly gross, but this makes sure that if the
// lazy-init returns an error it can be handled by the client
if independent {
new_resolver_system()
} else {
Ok(SHARED_RESOLVER
.fallback
.as_ref()
.ok_or(e)? // if the shared resolver has no fallback return the original error
.get_or_try_init(new_resolver_system)?
.clone())
}
})?;
resolver.lookup_ip(name.as_str()).await?
} else {
return Err(e.into());
}
}
};
async fn resolve(
name: Name,
resolver: Arc<OnceCell<TokioResolver>>,
maybe_fallback: Option<Arc<OnceCell<TokioResolver>>>,
maybe_static: Option<Arc<OnceCell<StaticResolver>>>,
independent: bool,
overall_dns_timeout: Duration,
) -> Result<Addrs, ResolveError> {
let resolver = resolver.get_or_try_init(|| HickoryDnsResolver::new_resolver(independent))?;
// Attempt a lookup using the primary resolver
let resolve_fut = tokio::time::timeout(overall_dns_timeout, resolver.lookup_ip(name.as_str()));
let primary_err = match resolve_fut.await {
Err(_) => ResolveError::Timeout,
Ok(Ok(lookup)) => {
let addrs: Addrs = Box::new(SocketAddrs {
iter: lookup.into_iter(),
});
Ok(addrs)
})
return Ok(addrs);
}
Ok(Err(e)) => {
// on failure use the fall back system configured DNS resolver
if !e.is_no_records_found() {
warn!("primary DNS failed w/ error: {e}");
}
e.into()
}
};
// If the primary resolver encountered an error, attempt a lookup using the fallback
// resolver if one is configured.
if let Some(ref fallback) = maybe_fallback {
let resolver =
fallback.get_or_try_init(|| HickoryDnsResolver::new_resolver_system(independent))?;
let resolve_fut =
tokio::time::timeout(overall_dns_timeout, resolver.lookup_ip(name.as_str()));
if let Ok(Ok(lookup)) = resolve_fut.await {
let addrs: Addrs = Box::new(SocketAddrs {
iter: lookup.into_iter(),
});
return Ok(addrs);
}
}
// If no record has been found and a static map of fallback addresses is configured
// check the table for our entry
if let Some(ref static_resolver) = maybe_static {
debug!("checking static");
let resolver =
static_resolver.get_or_init(|| HickoryDnsResolver::new_static_fallback(independent));
if let Ok(addrs) = resolver.resolve(name).await {
return Ok(addrs);
}
}
Err(primary_err)
}
struct SocketAddrs {
iter: LookupIpIntoIter,
}
impl Iterator for SocketAddrs {
@@ -162,28 +225,22 @@ impl Iterator for SocketAddrs {
impl HickoryDnsResolver {
/// Attempt to resolve a domain name to a set of ['IpAddr']s
pub async fn resolve_str(&self, name: &str) -> Result<LookupIp, HickoryDnsError> {
let resolver = self.state.get_or_try_init(|| self.new_resolver())?;
// try the primary DNS resolver that we set up (DoH or DoT or whatever)
let lookup = match resolver.lookup_ip(name).await {
Ok(res) => res,
Err(e) => {
if let Some(ref fallback) = self.fallback {
// on failure use the fall back system configured DNS resolver
if !e.is_no_records_found() {
warn!("primary DNS failed w/ error {e}: using system fallback");
}
let resolver = fallback.get_or_try_init(|| self.new_resolver_system())?;
resolver.lookup_ip(name).await?
} else {
return Err(e.into());
}
}
};
Ok(lookup)
pub async fn resolve_str(
&self,
name: &str,
) -> Result<impl Iterator<Item = IpAddr> + use<>, ResolveError> {
let n =
Name::from_str(name).map_err(|_| ResolveError::InvalidNameError(name.to_string()))?;
resolve(
n,
self.state.clone(),
self.fallback.clone(),
self.static_base.clone(),
self.dont_use_shared,
self.overall_dns_timeout,
)
.await
.map(|addrs| addrs.map(|socket_addr| socket_addr.ip()))
}
/// Create a (lazy-initialized) resolver that is not shared across threads.
@@ -194,16 +251,20 @@ impl HickoryDnsResolver {
}
}
fn new_resolver(&self) -> Result<TokioResolver, HickoryDnsError> {
if self.dont_use_shared {
fn new_resolver(dont_use_shared: bool) -> Result<TokioResolver, ResolveError> {
// using a closure here is slightly gross, but this makes sure that if the
// lazy-init returns an error it can be handled by the client
if dont_use_shared {
new_resolver()
} else {
Ok(SHARED_RESOLVER.state.get_or_try_init(new_resolver)?.clone())
}
}
fn new_resolver_system(&self) -> Result<TokioResolver, HickoryDnsError> {
if self.dont_use_shared || SHARED_RESOLVER.fallback.is_none() {
fn new_resolver_system(dont_use_shared: bool) -> Result<TokioResolver, ResolveError> {
// using a closure here is slightly gross, but this makes sure that if the
// lazy-init returns an error it can be handled by the client
if dont_use_shared || SHARED_RESOLVER.fallback.is_none() {
new_resolver_system()
} else {
Ok(SHARED_RESOLVER
@@ -215,8 +276,18 @@ impl HickoryDnsResolver {
}
}
fn new_static_fallback(dont_use_shared: bool) -> StaticResolver {
if !dont_use_shared && let Some(ref shared_resolver) = SHARED_RESOLVER.static_base {
shared_resolver
.get_or_init(new_default_static_fallback)
.clone()
} else {
new_default_static_fallback()
}
}
/// Enable fallback to the system default resolver if the primary (DoX) resolver fails
pub fn enable_system_fallback(&mut self) -> Result<(), HickoryDnsError> {
pub fn enable_system_fallback(&mut self) -> Result<(), ResolveError> {
self.fallback = Some(Default::default());
let _ = self
.fallback
@@ -231,22 +302,51 @@ impl HickoryDnsResolver {
pub fn disable_system_fallback(&mut self) {
self.fallback = None;
}
/// Get the current map of hostname to address in use by the fallback static lookup if one
/// exists.
pub fn get_static_fallbacks(&self) -> Option<HashMap<String, Vec<IpAddr>>> {
Some(self.static_base.as_ref()?.get()?.get_addrs())
}
/// Set (or overwrite) the map of addresses used in the fallback static hostname lookup
pub fn set_static_fallbacks(&mut self, addrs: HashMap<String, Vec<IpAddr>>) {
let cell = OnceCell::new();
cell.set(StaticResolver::new(addrs))
.expect("infallible assign");
self.static_base = Some(Arc::new(cell));
}
}
/// Create a new resolver with a custom DoT based configuration. The options are overridden to look
/// up for both IPv4 and IPv6 addresses to work with "happy eyeballs" algorithm.
fn new_resolver() -> Result<TokioResolver, HickoryDnsError> {
///
/// Timeout Defaults to 5 seconds
/// Number of retries after lookup failure before giving up Defaults to 2
///
/// Caches successfully resolved addresses for 30 minutes to prevent continual use of remote lookup.
/// This resolver is intended to be used for OUR API endpoints that do not rapidly rotate IPs.
fn new_resolver() -> Result<TokioResolver, ResolveError> {
info!("building new configured resolver");
let mut name_servers = NameServerConfigGroup::quad9_tls();
name_servers.merge(NameServerConfigGroup::quad9_https());
name_servers.merge(NameServerConfigGroup::cloudflare_tls());
name_servers.merge(NameServerConfigGroup::cloudflare_https());
configure_and_build_resolver(name_servers)
}
fn configure_and_build_resolver(
name_servers: NameServerConfigGroup,
) -> Result<TokioResolver, ResolveError> {
let config = ResolverConfig::from_parts(None, Vec::new(), name_servers);
let mut resolver_builder =
TokioResolver::builder_with_config(config, TokioConnectionProvider::default());
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
resolver_builder.options_mut().server_ordering_strategy = ServerOrderingStrategy::RoundRobin;
// Cache successful responses for queries received by this resolver for 30 min minimum.
resolver_builder.options_mut().positive_min_ttl = Some(Duration::from_secs(1800));
Ok(resolver_builder.build())
}
@@ -254,20 +354,27 @@ fn new_resolver() -> Result<TokioResolver, HickoryDnsError> {
/// Create a new resolver with the default configuration, which reads from the system DNS config
/// (i.e. `/etc/resolve.conf` in unix). The options are overridden to look up for both IPv4 and IPv6
/// addresses to work with "happy eyeballs" algorithm.
fn new_resolver_system() -> Result<TokioResolver, HickoryDnsError> {
fn new_resolver_system() -> Result<TokioResolver, ResolveError> {
let mut resolver_builder = TokioResolver::builder_tokio()?;
resolver_builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
Ok(resolver_builder.build())
}
fn new_default_static_fallback() -> StaticResolver {
StaticResolver::new(constants::default_static_addrs())
}
#[cfg(test)]
mod test {
use super::*;
use itertools::Itertools;
use std::collections::HashMap;
#[tokio::test]
async fn reqwest_hickory_doh() {
let resolver = HickoryDnsResolver::default();
async fn reqwest_with_custom_dns() {
let var_name = HickoryDnsResolver::default();
let resolver = var_name;
let client = reqwest::ClientBuilder::new()
.dns_resolver(resolver.into())
.build()
@@ -286,7 +393,7 @@ mod test {
}
#[tokio::test]
async fn dns_lookup() -> Result<(), HickoryDnsError> {
async fn dns_lookup() -> Result<(), ResolveError> {
let resolver = HickoryDnsResolver::default();
let domain = "ifconfig.me";
@@ -296,4 +403,124 @@ mod test {
Ok(())
}
#[tokio::test]
async fn static_resolver_as_fallback() -> Result<(), ResolveError> {
let example_domain = "non-existent.nymvpn.com";
let mut resolver = HickoryDnsResolver {
..Default::default()
};
let result = resolver.resolve_str(example_domain).await;
assert!(result.is_err()); // should be NXDomain
resolver.static_base = Some(Default::default());
let mut addr_map = HashMap::new();
let example_ip4: IpAddr = "10.10.10.10".parse().unwrap();
let example_ip6: IpAddr = "dead::beef".parse().unwrap();
addr_map.insert(example_domain.to_string(), vec![example_ip4, example_ip6]);
resolver.set_static_fallbacks(addr_map);
let mut addrs = resolver.resolve_str(example_domain).await?;
assert!(addrs.contains(&example_ip4));
assert!(addrs.contains(&example_ip6));
Ok(())
}
}
#[cfg(test)]
mod failure_test {
use super::*;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
/// IP addresses guaranteed to fail attempts to resolve
///
/// Addresses drawn from blocks set off by RFC5737 (ipv4) and RFC3849 (ipv6)
const GUARANTEED_BROKEN_IPS_1: &[IpAddr] = &[
IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)),
IpAddr::V4(Ipv4Addr::new(198, 51, 100, 1)),
IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x1111)),
IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0x1001)),
];
// Create a resolver that behaves the same as the custom configured router, except for the fact
// that it is guaranteed to fail.
fn build_broken_resolver() -> Result<TokioResolver, ResolveError> {
info!("building new faulty resolver");
let mut broken_ns_group = NameServerConfigGroup::from_ips_tls(
GUARANTEED_BROKEN_IPS_1,
853,
"cloudflare-dns.com".to_string(),
true,
);
let broken_ns_https = NameServerConfigGroup::from_ips_https(
GUARANTEED_BROKEN_IPS_1,
443,
"cloudflare-dns.com".to_string(),
true,
);
broken_ns_group.merge(broken_ns_https);
configure_and_build_resolver(broken_ns_group)
}
#[tokio::test]
async fn dns_lookup_failures() -> Result<(), ResolveError> {
let time_start = std::time::Instant::now();
let r = OnceCell::new();
r.set(build_broken_resolver().expect("failed to build resolver"))
.expect("broken resolver init error");
// create a new resolver that won't mess with the shared resolver used by other tests
let resolver = HickoryDnsResolver {
dont_use_shared: true,
state: Arc::new(r),
overall_dns_timeout: Duration::from_secs(5),
..Default::default()
};
build_broken_resolver()?;
let domain = "ifconfig.me";
let result = resolver.resolve_str(domain).await;
assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
let duration = time_start.elapsed();
assert!(duration < resolver.overall_dns_timeout + Duration::from_secs(1));
Ok(())
}
#[tokio::test]
async fn fallback_to_static() -> Result<(), ResolveError> {
let r = OnceCell::new();
r.set(build_broken_resolver().expect("failed to build resolver"))
.expect("broken resolver init error");
// create a new resolver that won't mess with the shared resolver used by other tests
let resolver = HickoryDnsResolver {
dont_use_shared: true,
state: Arc::new(r),
static_base: Some(Default::default()),
overall_dns_timeout: Duration::from_secs(5),
..Default::default()
};
build_broken_resolver()?;
// successful lookup using fallback to static resolver
let domain = "nymvpn.com";
let _ = resolver
.resolve_str(domain)
.await
.expect("failed to resolve address in static lookup");
// unsuccessful lookup - primary times out, and not in
let domain = "non-existent.nymtech.net";
let result = resolver.resolve_str(domain).await;
assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
Ok(())
}
}
@@ -0,0 +1,95 @@
#![allow(missing_docs)]
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
pub const NYM_API_DOMAIN: &str = "validator.nymtech.net";
pub const NYM_API_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(212, 71, 233, 232))];
pub const NYM_VPN_API_DOMAIN: &str = "nymvpn.com";
pub const NYM_VPN_API_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(76, 76, 21, 21))];
pub const NYM_FRONTDOOR_VERCEL_DOMAIN: &str = "nym-frontdoor.vercel.app";
pub const NYM_FRONTDOOR_VERCEL_IPS: &[IpAddr] = &[
IpAddr::V4(Ipv4Addr::new(64, 29, 17, 195)),
IpAddr::V4(Ipv4Addr::new(216, 198, 79, 195)),
];
pub const NYM_FRONTDOOR_FASTLY_DOMAIN: &str = "nym-frontdoor.global.ssl.fastly.net";
pub const NYM_FRONTDOOR_FASTLY_IPS: &[IpAddr] = &[
IpAddr::V4(Ipv4Addr::new(151, 101, 193, 194)),
IpAddr::V4(Ipv4Addr::new(151, 101, 129, 194)),
IpAddr::V4(Ipv4Addr::new(151, 101, 1, 194)),
IpAddr::V4(Ipv4Addr::new(151, 101, 65, 194)),
];
pub const NYMVPN_FRONTDOOR_FASTLY_DOMAIN: &str = "nymvpn-frontdoor.global.ssl.fastly.net";
pub const NYMVPN_FRONTDOOR_FASTLY_IPS: &[IpAddr] = &[
IpAddr::V4(Ipv4Addr::new(151, 101, 193, 194)),
IpAddr::V4(Ipv4Addr::new(151, 101, 129, 194)),
IpAddr::V4(Ipv4Addr::new(151, 101, 1, 194)),
IpAddr::V4(Ipv4Addr::new(151, 101, 65, 194)),
];
pub const YELP_FASTLY_DOMAIN: &str = "yelp.global.ssl.fastly.net";
pub const YELP_FASTLY_IPS: &[IpAddr] = &[
IpAddr::V4(Ipv4Addr::new(151, 101, 193, 194)),
IpAddr::V4(Ipv4Addr::new(151, 101, 129, 194)),
IpAddr::V4(Ipv4Addr::new(151, 101, 1, 194)),
IpAddr::V4(Ipv4Addr::new(151, 101, 65, 194)),
];
pub const VERCEL_APP_DOMAIN: &str = "vercel.app";
pub const VERCEL_APP_IPS: &[IpAddr] = &[
IpAddr::V4(Ipv4Addr::new(64, 29, 17, 195)),
IpAddr::V4(Ipv4Addr::new(216, 198, 79, 195)),
];
pub const VERCEL_COM_DOMAIN: &str = "vercel.com";
pub const VERCEL_COM_IPS: &[IpAddr] = &[
IpAddr::V4(Ipv4Addr::new(198, 169, 2, 129)),
IpAddr::V4(Ipv4Addr::new(198, 169, 1, 193)),
];
pub const NYM_COM_DOMAIN: &str = "nym.com";
pub const NYM_COM_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(76, 76, 21, 22))];
pub const NYM_STATS_API_DOMAIN: &str = "nym-statistics-api.nymtech.cc";
pub const NYM_STATS_API_IPS: &[IpAddr] = &[IpAddr::V4(Ipv4Addr::new(91, 92, 153, 96))];
pub const NYM_RPC_DOMAIN: &str = "rpc.nymtech.net";
pub const NYM_RPC_IPS: &[IpAddr] = &[
IpAddr::V4(Ipv4Addr::new(194, 182, 169, 49)),
IpAddr::V4(Ipv4Addr::new(91, 92, 200, 116)),
IpAddr::V6(Ipv6Addr::new(
0x2a04, 0xc43, 0xe00, 0x6f28, 0x400, 0xd8ff, 0xfe00, 0x1483,
)),
IpAddr::V6(Ipv6Addr::new(
0x2a04, 0xc46, 0xe00, 0x6f28, 0x4b3, 0x68ff, 0xfe00, 0x460,
)),
];
pub fn default_static_addrs() -> HashMap<String, Vec<IpAddr>> {
let mut m = HashMap::new();
m.insert(NYM_API_DOMAIN.to_string(), NYM_API_IPS.to_vec());
m.insert(NYM_VPN_API_DOMAIN.to_string(), NYM_VPN_API_IPS.to_vec());
m.insert(
NYM_FRONTDOOR_VERCEL_DOMAIN.to_string(),
NYM_FRONTDOOR_VERCEL_IPS.to_vec(),
);
m.insert(
NYM_FRONTDOOR_FASTLY_DOMAIN.to_string(),
NYM_FRONTDOOR_FASTLY_IPS.to_vec(),
);
m.insert(
NYMVPN_FRONTDOOR_FASTLY_DOMAIN.to_string(),
NYMVPN_FRONTDOOR_FASTLY_IPS.to_vec(),
);
m.insert(YELP_FASTLY_DOMAIN.to_string(), YELP_FASTLY_IPS.to_vec());
m.insert(VERCEL_APP_DOMAIN.to_string(), VERCEL_APP_IPS.to_vec());
m.insert(VERCEL_COM_DOMAIN.to_string(), VERCEL_COM_IPS.to_vec());
m.insert(NYM_COM_DOMAIN.to_string(), NYM_COM_IPS.to_vec());
m.insert(NYM_STATS_API_DOMAIN.to_string(), NYM_STATS_API_IPS.to_vec());
m.insert(NYM_RPC_DOMAIN.to_string(), NYM_RPC_IPS.to_vec());
m
}
@@ -0,0 +1,89 @@
use crate::dns::ResolveError;
use std::{
collections::HashMap,
net::{IpAddr, SocketAddr},
sync::{Arc, Mutex},
};
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
use tracing::*;
#[derive(Debug, Default, Clone)]
pub struct StaticResolver {
static_addr_map: Arc<Mutex<HashMap<String, Vec<IpAddr>>>>,
}
impl StaticResolver {
pub fn new(static_entries: HashMap<String, Vec<IpAddr>>) -> StaticResolver {
debug!("building static resolver");
Self {
static_addr_map: Arc::new(Mutex::new(static_entries)),
}
}
pub fn get_addrs(&self) -> HashMap<String, Vec<IpAddr>> {
self.static_addr_map.lock().unwrap().clone()
}
}
impl Resolve for StaticResolver {
fn resolve(&self, name: Name) -> Resolving {
debug!("looking up {name:?} in static resolver");
let addr_map = self.static_addr_map.clone();
Box::pin(async move {
let addr_map = addr_map.lock().unwrap();
let lookup = match addr_map.get(name.as_str()) {
None => return Err(ResolveError::StaticLookupMiss.into()),
Some(addrs) => addrs,
};
let addrs: Addrs = Box::new(
lookup
.clone()
.into_iter()
.map(|ip_addr| SocketAddr::new(ip_addr, 0)),
);
Ok(addrs)
})
}
}
#[cfg(test)]
mod test {
use itertools::Itertools;
use super::*;
use std::error::Error as StdError;
use std::str::FromStr;
#[tokio::test]
async fn lookup_using_static_resolver() -> Result<(), Box<dyn StdError + Send + Sync>> {
let example_domain = String::from("static.nymvpn.com");
// lookup for domain for which there is no entry
let resolver = StaticResolver::new(HashMap::new());
let url = reqwest::dns::Name::from_str(&example_domain).unwrap();
let result = resolver.resolve(url).await;
assert!(result.is_err());
match result {
Ok(_) => panic!("lookup with empty map should fail"),
Err(e) => assert_eq!(e.to_string(), ResolveError::StaticLookupMiss.to_string()),
}
// Successful lookup
let mut addr_map = HashMap::new();
let example_ip4: IpAddr = "10.10.10.10".parse().unwrap();
let example_ip6: IpAddr = "dead::beef".parse().unwrap();
addr_map.insert(example_domain.clone(), vec![example_ip4, example_ip6]);
let url = reqwest::dns::Name::from_str(&example_domain).unwrap();
let resolver = StaticResolver::new(addr_map);
let mut addrs = resolver.resolve(url).await?;
assert!(addrs.contains(&SocketAddr::new(example_ip4, 0)));
assert!(addrs.contains(&SocketAddr::new(example_ip6, 0)));
Ok(())
}
}
+85
View File
@@ -121,4 +121,89 @@ mod tests {
// println!("{response:?}");
assert_eq!(response.status(), 200);
}
#[tokio::test]
async fn fallback_on_failure() {
let url1 = Url::new(
"https://fake-domain.nymtech.net",
Some(vec![
"https://fake-front-1.nymtech.net",
"https://fake-front-2.nymtech.net",
]),
)
.unwrap();
let url2 = Url::new(
"https://validator.global.ssl.fastly.net",
Some(vec!["https://yelp.global.ssl.fastly.net"]),
)
.unwrap(); // fastly
let client = ClientBuilder::new_with_urls(vec![url1, url2])
.expect("bad url")
.with_fronting(FrontPolicy::Always)
.build()
.expect("failed to build client");
// Check that the initial configuration has the broken domain and front.
assert_eq!(
client.current_url().as_str(),
"https://fake-domain.nymtech.net/",
);
assert_eq!(
client.current_url().front_str(),
Some("fake-front-1.nymtech.net"),
);
let result = client
.send_request::<_, (), &str, &str>(
reqwest::Method::GET,
&["api", "v1", "network", "details"],
NO_PARAMS,
None,
)
.await;
assert!(result.is_err());
// Check that the host configuration updated the front on error.
assert_eq!(
client.current_url().as_str(),
"https://fake-domain.nymtech.net/",
);
assert_eq!(
client.current_url().front_str(),
Some("fake-front-2.nymtech.net"),
);
let result = client
.send_request::<_, (), &str, &str>(
reqwest::Method::GET,
&["api", "v1", "network", "details"],
NO_PARAMS,
None,
)
.await;
assert!(result.is_err());
// Check that the host configuration updated the domain and front on error.
assert_eq!(
client.current_url().as_str(),
"https://validator.global.ssl.fastly.net/",
);
assert_eq!(
client.current_url().front_str(),
Some("yelp.global.ssl.fastly.net"),
);
let response = client
.send_request::<_, (), &str, &str>(
reqwest::Method::GET,
&["api", "v1", "network", "details"],
NO_PARAMS,
None,
)
.await
.expect("failed get request");
assert_eq!(response.status(), 200);
}
}
+89 -17
View File
@@ -179,7 +179,7 @@ mod dns;
mod path;
#[cfg(not(target_arch = "wasm32"))]
pub use dns::{HickoryDnsError, HickoryDnsResolver};
pub use dns::{HickoryDnsResolver, ResolveError};
// helper for generating user agent based on binary information
#[cfg(not(target_arch = "wasm32"))]
@@ -395,6 +395,13 @@ pub enum HttpClientError {
#[error("failed to resolve request to {url} due to data inconsistency: {details}")]
InternalResponseInconsistency { url: ::url::Url, details: String },
#[cfg(not(target_arch = "wasm32"))]
#[error("encountered dns failure: {inner}")]
DnsLookupFailure {
#[from]
inner: ResolveError,
},
#[error("Failed to encode bincode: {0}")]
Bincode(#[from] bincode::Error),
@@ -421,6 +428,8 @@ impl HttpClientError {
HttpClientError::ReqwestClientError { source } => source.is_timeout(),
HttpClientError::RequestSendFailure { source, .. } => source.0.is_timeout(),
HttpClientError::ResponseReadFailure { source, .. } => source.0.is_timeout(),
#[cfg(not(target_arch = "wasm32"))]
HttpClientError::DnsLookupFailure { inner } => inner.is_timeout(),
#[cfg(target_arch = "wasm32")]
HttpClientError::RequestTimeout => true,
_ => false,
@@ -775,6 +784,7 @@ impl ClientBuilder {
base_urls: self.urls,
current_idx: Arc::new(AtomicUsize::new(0)),
reqwest_client,
using_secure_dns: self.use_secure_dns,
#[cfg(feature = "tunneling")]
front: self.front,
@@ -795,6 +805,7 @@ pub struct Client {
base_urls: Vec<Url>,
current_idx: Arc<AtomicUsize>,
reqwest_client: reqwest::Client,
using_secure_dns: bool,
#[cfg(feature = "tunneling")]
front: Option<fronted::Front>,
@@ -852,6 +863,7 @@ impl Client {
base_urls: vec![new_url],
current_idx: Arc::new(Default::default()),
reqwest_client: self.reqwest_client.clone(),
using_secure_dns: self.using_secure_dns,
#[cfg(feature = "tunneling")]
front: self.front.clone(),
@@ -883,8 +895,34 @@ impl Client {
self.retry_limit = limit;
}
fn matches_current_host(&self, url: &Url) -> bool {
if cfg!(feature = "tunneling") {
if let Some(ref front) = self.front
&& front.is_enabled()
{
url.host_str() == self.current_url().front_str()
} else {
url.host_str() == self.current_url().host_str()
}
} else {
url.host_str() == self.current_url().host_str()
}
}
/// If multiple base urls are available rotate to next (e.g. when the current one resulted in an error)
fn update_host(&self) {
///
/// Takes an optional URL argument. If this is none, the current host will be updated automatically.
/// If a url is provided first check that the CURRENT host matches the hostname in the URL before
/// triggering a rotation. This is meant to prevent parallel requests that fail from rotating the host
/// multiple times.
fn update_host(&self, maybe_url: Option<Url>) {
// If a causal url is provided and it doesn't match the hostname currently in use, skip update.
if let Some(err_url) = maybe_url
&& !self.matches_current_host(&err_url)
{
return;
}
#[cfg(feature = "tunneling")]
if let Some(ref front) = self.front
&& front.is_enabled()
@@ -1048,8 +1086,28 @@ impl ApiClientCore for Client {
.build()
.map_err(HttpClientError::reqwest_client_build_error)?;
self.apply_hosts_to_req(&mut req);
let url: Url = req.url().clone().into();
// try an explicit DNS resolution - if successful then it will be in cache when reqwest
// goes to execute the request. If failure then we get to handle the DNS lookup error.
#[cfg(not(target_arch = "wasm32"))]
let url = req.url().clone();
if self.using_secure_dns
&& let Some(hostname) = req.url().domain()
// Default here will use a shared resolver instance
&& let Err(err) = HickoryDnsResolver::default().resolve_str(hostname).await
{
// on failure update host, but don't trigger fronting enable.
self.update_host(Some(url.clone()));
if attempts < self.retry_limit {
attempts += 1;
warn!(
"Retrying request due to dns error on attempt ({attempts}/{}): {err}",
self.retry_limit
);
continue;
}
}
#[cfg(target_arch = "wasm32")]
let response: Result<Response, HttpClientError> = {
@@ -1067,25 +1125,39 @@ impl ApiClientCore for Client {
match response {
Ok(resp) => return Ok(resp),
Err(err) => {
// if we have multiple urls, update to the next
self.update_host();
// only if there was a network issue should we consider updating the host info
//
// note: for now this includes DNS resolution failure, I am not sure how I would go about
// segregating that based on the interface provided by request for errors.
#[cfg(target_arch = "wasm32")]
let is_network_err = err.is_timeout();
#[cfg(not(target_arch = "wasm32"))]
let is_network_err = err.is_timeout() || err.is_connect();
#[cfg(feature = "tunneling")]
if let Some(ref front) = self.front {
// If fronting is set to be enabled on error, enable domain fronting as we
// have encountered an error.
let was_enabled = front.is_enabled();
front.retry_enable();
if !was_enabled && front.is_enabled() {
tracing::info!(
"Domain fronting activated after connection failure: {err}",
);
if is_network_err {
// if we have multiple urls, update to the next
self.update_host(Some(url.clone()));
#[cfg(feature = "tunneling")]
if let Some(ref front) = self.front {
// If fronting is set to be enabled on error, enable domain fronting as we
// have encountered an error.
let was_enabled = front.is_enabled();
front.retry_enable();
if !was_enabled && front.is_enabled() {
tracing::info!(
"Domain fronting activated after connection failure: {err}",
);
}
}
}
if attempts < self.retry_limit {
warn!("Retrying request due to http error: {err}");
attempts += 1;
warn!(
"Retrying request due to http error on attempt ({attempts}/{}): {err}",
self.retry_limit
);
continue;
}
@@ -1094,7 +1166,7 @@ impl ApiClientCore for Client {
if #[cfg(target_arch = "wasm32")] {
return Err(err);
} else {
return Err(HttpClientError::request_send_error(url, err));
return Err(HttpClientError::request_send_error(url.into(), err));
}
}
}
+35 -11
View File
@@ -91,14 +91,15 @@ fn sanitizing_urls() {
#[tokio::test]
async fn api_client_retry() -> Result<(), Box<dyn std::error::Error>> {
let client = ClientBuilder::new_with_urls(vec![
"http://broken.nym.test".parse()?, // This will fail
"https://httpbin.org/status/200".parse()?, // This will succeed
"http://broken.nym.test".parse()?, // This will fail because of DNS (rotate)
"http://127.0.0.1:9".parse()?, // This will fail because of TCP refused (rotate)
"https://httpbin.org/status/200".parse()?, // This should succeed
])?
.with_retries(3)
.build()?;
let req = client.create_get_request(&[], NO_PARAMS).unwrap();
let resp = client.send(req).await?;
let _resp = client.send(req).await?;
// The main test is that we successfully retried and switched to the working URL
// We accept any response from the working endpoint since external services can be unreliable
@@ -107,7 +108,9 @@ async fn api_client_retry() -> Result<(), Box<dyn std::error::Error>> {
"https://httpbin.org/status/200"
);
println!("Response status: {}", resp.status());
// // This assert can be unreliable due to factors beyond our control and beyond the scope of
// // this test
// assert_eq!(_resp.status(), StatusCode::OK);
Ok(())
}
@@ -123,7 +126,7 @@ fn host_updating() {
assert_eq!(current_url.front_str(), None);
// update the url
client.update_host();
client.update_host(None);
// check that the url is still the same since there is one URL
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
@@ -138,13 +141,13 @@ fn host_updating() {
client.change_base_urls(new_urls);
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
client.update_host();
client.update_host(None);
// check that the url got updated now that there are multiple URLs
assert_eq!(client.current_url().as_str(), "http://nym-api2.test/");
assert_eq!(client.current_url().front_str(), None);
client.update_host();
client.update_host(None);
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
// =======================================
@@ -162,12 +165,33 @@ fn host_updating() {
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
client.update_host();
client.update_host(None);
// check that the url got updated now that there are multiple URLs
assert_eq!(client.current_url().as_str(), "http://nym-api2.test/");
}
#[test]
fn host_updating_url_conditioned() {
let url1 = Url::new("http://nym-api1.test", None).unwrap();
let url2 = Url::new("http://nym-api2.test", None).unwrap();
let urls = vec![url1.clone(), url2.clone()];
let client = ClientBuilder::new_with_urls(urls).unwrap().build().unwrap();
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
// Try to update with a URL that does NOT match current - should result in no change
client.update_host(Some(Url::parse("http://example.com").unwrap()));
// check that the url did NOT get updated
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
assert_eq!(client.current_url().front_str(), None);
// Try to update with a URL that DOES match current - should result in no change
client.update_host(Some(url1));
assert_eq!(client.current_url().as_str(), "http://nym-api2.test/");
}
#[test]
#[cfg(feature = "tunneling")]
fn fronted_host_updating() {
@@ -184,7 +208,7 @@ fn fronted_host_updating() {
assert_eq!(current_url.front_str(), Some("cdn1.test"));
// update the url
client.update_host();
client.update_host(None);
// check that the url is still the same since there is one URL and one front
let current_url = client.current_url();
@@ -209,7 +233,7 @@ fn fronted_host_updating() {
assert_eq!(current_url.front_str(), Some("cdn1.test"));
// update the url - this should keep the same host but change the front
client.update_host();
client.update_host(None);
let current_url = client.current_url();
// check that the url is still the same since there is one URL
@@ -217,7 +241,7 @@ fn fronted_host_updating() {
assert_eq!(current_url.front_str(), Some("cdn2.test"));
// update the url - this should wrap around to the first front as the second url is not fronted
client.update_host();
client.update_host(None);
let current_url = client.current_url();
assert_eq!(current_url.as_str(), "http://nym-api.test/");
+22
View File
@@ -54,6 +54,12 @@ pub const NYM_APIS: &[ApiUrlConst] = &[
];
pub const NYM_VPN_API: &str = "https://nymvpn.com/api/";
pub const UPGRADE_MODE_ATTESTATION_URL: &str =
"https://nymtech.net/.wellknown/upgrade-mode/attestation.json";
pub const UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY: &str =
"3bgffBYcfFkTTXc2npNNn9MkddFZ3H2LrPjXDmnJzrqd";
#[cfg(feature = "network")]
pub const NYM_VPN_APIS: &[ApiUrlConst] = &[
ApiUrlConst {
@@ -159,6 +165,14 @@ pub fn export_to_env() {
set_var_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS);
set_var_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL);
set_var_to_default(var_names::NYM_VPN_API, NYM_VPN_API);
set_var_to_default(
var_names::UPGRADE_MODE_ATTESTATION_URL,
UPGRADE_MODE_ATTESTATION_URL,
);
set_var_to_default(
var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
);
}
#[cfg(all(feature = "env", feature = "network"))]
@@ -199,4 +213,12 @@ pub fn export_to_env_if_not_set() {
set_var_conditionally_to_default(var_names::NYM_API, NYM_API);
set_var_conditionally_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS);
set_var_conditionally_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL);
set_var_conditionally_to_default(
var_names::UPGRADE_MODE_ATTESTATION_URL,
UPGRADE_MODE_ATTESTATION_URL,
);
set_var_conditionally_to_default(
var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
);
}
+2
View File
@@ -25,6 +25,8 @@ pub const NYXD_WEBSOCKET: &str = "NYXD_WS";
pub const EXIT_POLICY_URL: &str = "EXIT_POLICY";
pub const NYM_VPN_API: &str = "NYM_VPN_API";
pub const CLIENT_STATS_COLLECTION_PROVIDER: &str = "CLIENT_STATS_COLLECTION_PROVIDER";
pub const UPGRADE_MODE_ATTESTATION_URL: &str = "UPGRADE_MODE_ATTESTATION_URL";
pub const UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY: &str = "UPGRADE_MODE_ATTESTER_ED25519_PUBKEY";
pub const DKG_TIME_CONFIGURATION: &str = "DKG_TIME_CONFIGURATION";
@@ -82,6 +82,17 @@ pub struct Protocol {
pub service_provider_type: ServiceProviderType,
}
impl Protocol {
pub const fn new(version: u8, service_provider_type: ServiceProviderType) -> Self {
Self {
version,
service_provider_type,
}
}
}
// NOTE: this only works under the assumption of using bincode for serialisation
// with the current field layout
impl TryFrom<&[u8; 2]> for Protocol {
type Error = ProtocolError;
+5 -2
View File
@@ -6,9 +6,12 @@ use thiserror::Error;
#[derive(Debug, Error)]
pub enum UpgradeModeCheckError {
#[error("failed to decode jwt metadata")]
#[error("failed to decode jwt metadata: {source}")]
TokenMetadataDecodeFailure { source: jwt_simple::Error },
#[error("the upgrade mode JWT is malformed")]
MalformedToken,
#[error("the jwt metadata didn't contain explicit public key")]
MissingTokenPublicKey,
@@ -21,6 +24,6 @@ pub enum UpgradeModeCheckError {
#[error("failed to verify the jwt: {source}")]
JwtVerificationFailure { source: jwt_simple::Error },
#[error("failed to retrieve attestation from {url}:{source}")]
#[error("failed to retrieve attestation from {url}: {source}")]
AttestationRetrievalFailure { url: String, source: reqwest::Error },
}
+79 -4
View File
@@ -4,12 +4,17 @@
use crate::{UpgradeModeAttestation, UpgradeModeCheckError};
use jwt_simple::claims::Claims;
use jwt_simple::common::{KeyMetadata, VerificationOptions};
use jwt_simple::prelude::{EdDSAKeyPairLike, EdDSAPublicKeyLike};
use jwt_simple::prelude::{
Base64UrlSafeNoPadding, EdDSAKeyPairLike, EdDSAPublicKeyLike, JWTClaims,
};
use jwt_simple::reexports::ct_codecs::Decoder;
use jwt_simple::token::Token;
use nym_crypto::asymmetric::ed25519;
use std::collections::HashSet;
use std::time::Duration;
pub const CREDENTIAL_PROXY_JWT_ISSUER: &str = "nym-credential-proxy";
// for now use static issuer such as "nym-credential-proxy"
pub fn generate_jwt_for_upgrade_mode_attestation(
attestation: UpgradeModeAttestation,
@@ -74,12 +79,26 @@ pub fn validate_upgrade_mode_jwt(
Ok(attestation)
}
/// Attempt to extract the upgrade mode JWT payload from the provided token
pub fn try_decode_upgrade_mode_jwt_claims(
token: &str,
) -> Result<JWTClaims<UpgradeModeAttestation>, UpgradeModeCheckError> {
let mut parts = token.split('.');
let _header = parts.next().ok_or(UpgradeModeCheckError::MalformedToken)?;
let claims_b64 = parts.next().ok_or(UpgradeModeCheckError::MalformedToken)?;
let claims_bytes = Base64UrlSafeNoPadding::decode_to_vec(claims_b64, None)
.map_err(|_| UpgradeModeCheckError::MalformedToken)?;
serde_json::from_slice(&claims_bytes).map_err(|_| UpgradeModeCheckError::MalformedToken)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::generate_new_attestation;
use nym_crypto::asymmetric::ed25519;
use nym_test_utils::helpers::deterministic_rng;
use time::OffsetDateTime;
#[test]
fn generate_and_validate_jwt() {
@@ -109,11 +128,11 @@ mod tests {
attestation.clone(),
Duration::from_secs(60 * 60),
&unauthorised_jwt_keys,
Some("nym-credential-proxy"),
Some(CREDENTIAL_PROXY_JWT_ISSUER),
);
// we expect 'nym-credential-proxy' issuer
assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some("nym-credential-proxy")).is_ok());
assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some(CREDENTIAL_PROXY_JWT_ISSUER)).is_ok());
// we don't care about issuer
assert!(validate_upgrade_mode_jwt(&jwt_issuer, None).is_ok());
@@ -133,9 +152,65 @@ mod tests {
None,
);
// we expect 'nym-credential-proxy' issuer
assert!(validate_upgrade_mode_jwt(&jwt_no_issuer, Some("nym-credential-proxy")).is_err());
assert!(
validate_upgrade_mode_jwt(&jwt_no_issuer, Some(CREDENTIAL_PROXY_JWT_ISSUER)).is_err()
);
// we don't care about issuer
assert!(validate_upgrade_mode_jwt(&jwt_no_issuer, None).is_ok());
}
#[test]
fn decode_upgrade_mode_claims() {
let invalid_jwts = [
"",
"invalidSections",
"also.invalid.sections",
"eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImp3ayI6IkZCdWsxS2lqS3ZwQ3VrU1Zhc0xoN1k1REZTZEdnVzU5WThQOUhWTDh2Mzk5In0.eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImp3ayI6IkZCdWsxS2lqS3ZwQ3VrU1Zhc0xoN1k1REZTZEdnVzU5WThQOUhWTDh2Mzk5In0.eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImp3ayI6IkZCdWsxS2lqS3ZwQ3VrU1Zhc0xoN1k1REZTZEdnVzU5WThQOUhWTDh2Mzk5In0",
];
let attestation_key = ed25519::PrivateKey::from_bytes(&[
108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248,
163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161,
])
.unwrap();
let jwt_key = ed25519::PrivateKey::from_bytes(&[
152, 17, 144, 255, 213, 219, 246, 208, 109, 33, 100, 73, 1, 141, 32, 63, 141, 89, 167,
2, 52, 215, 241, 219, 200, 18, 159, 241, 76, 111, 42, 32,
])
.unwrap();
let jwt_keys = ed25519::KeyPair::from(jwt_key);
let validity = Duration::from_secs(60 * 60);
let attestation = generate_new_attestation(&attestation_key, vec![*jwt_keys.public_key()]);
let valid_jwt = generate_jwt_for_upgrade_mode_attestation(
attestation.clone(),
validity,
&jwt_keys,
Some("nym-credential-proxy"),
);
for invalid in invalid_jwts {
assert!(try_decode_upgrade_mode_jwt_claims(invalid).is_err())
}
let decoded = try_decode_upgrade_mode_jwt_claims(&valid_jwt).unwrap();
assert_eq!(decoded.issuer.unwrap(), "nym-credential-proxy");
assert_eq!(decoded.custom, attestation);
// unfortunately we can't inject current time when constructing the JWT so the best we can do is ensure its within error margin
let margin = Duration::from_secs(10);
let now = OffsetDateTime::now_utc();
let min = now - margin;
let max = now + margin;
let issued = decoded.issued_at.unwrap();
let issued_time = OffsetDateTime::from_unix_timestamp(issued.as_secs() as i64).unwrap();
assert!(issued_time >= min && issued_time <= max);
let min = now - margin + validity;
let max = now + margin + validity;
let expires = decoded.expires_at.unwrap();
let expires_time = OffsetDateTime::from_unix_timestamp(expires.as_secs() as i64).unwrap();
assert!(expires_time >= min && expires_time <= max);
}
}
+6 -1
View File
@@ -9,7 +9,12 @@ pub use attestation::{
UpgradeModeAttestation, generate_new_attestation, generate_new_attestation_with_starting_time,
};
pub use error::UpgradeModeCheckError;
pub use jwt::{generate_jwt_for_upgrade_mode_attestation, validate_upgrade_mode_jwt};
pub use jwt::{
CREDENTIAL_PROXY_JWT_ISSUER, generate_jwt_for_upgrade_mode_attestation,
try_decode_upgrade_mode_jwt_claims, validate_upgrade_mode_jwt,
};
#[cfg(not(target_arch = "wasm32"))]
pub use attestation::attempt_retrieve_attestation;
pub const UPGRADE_MODE_CREDENTIAL_TYPE: &str = "upgrade_mode_jwt";
@@ -46,6 +46,23 @@ pub trait WireguardMetadataApiClient: ApiClient {
)
.await
}
#[instrument(level = "debug", skip(self, request_body))]
async fn request_upgrade_mode_check(
&self,
request_body: &Request,
) -> Result<Response, HttpClientError> {
self.post_json(
&[
routes::V1_API_VERSION,
routes::NETWORK,
routes::UPGRADE_MODE_CHECK,
],
NO_PARAMS,
request_body,
)
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -15,7 +15,7 @@ use utoipa_swagger_ui::SwaggerUi;
use crate::http::openapi::ApiDoc;
use crate::http::state::AppState;
use crate::network::bandwidth_routes;
use crate::network::{bandwidth_routes, network_routes};
/// Wrapper around `axum::Router` which ensures correct [order of layers][order].
/// Add new routes as if you were working directly with `axum`.
@@ -35,7 +35,12 @@ impl RouterBuilder {
let default_routes = Router::new()
.merge(SwaggerUi::new("/swagger").url("/api-docs/openapi.json", ApiDoc::openapi()))
.route("/", get(|| async { Redirect::to("/swagger") }))
.nest("/v1", Router::new().nest("/bandwidth", bandwidth_routes()));
.nest(
"/v1",
Router::new()
.nest("/bandwidth", bandwidth_routes())
.nest("/network", network_routes()),
);
Self {
unfinished_router: default_routes,
}
@@ -1,35 +1,137 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_credential_verification::upgrade_mode::UpgradeModeDetails;
use nym_credentials_interface::BandwidthCredential;
use std::cmp::max;
use std::net::IpAddr;
use nym_credentials_interface::CredentialSpendingData;
use crate::transceiver::PeerControllerTransceiver;
use nym_wireguard_private_metadata_shared::error::MetadataError;
use nym_wireguard_private_metadata_shared::interface::{ResponseData, UpgradeModeCheckRequestType};
// we need to be above MINIMUM_REMAINING_BANDWIDTH (500MB) plus we also have to trick the client
// its depletion is low enough to not require sending new tickets
const DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD: i64 = 1024 * 1024 * 1024;
#[derive(Clone, axum::extract::FromRef)]
pub struct AppState {
transceiver: PeerControllerTransceiver,
#[from_ref(skip)]
upgrade_mode: UpgradeModeDetails,
}
impl AppState {
pub fn new(transceiver: PeerControllerTransceiver) -> Self {
Self { transceiver }
pub fn new(transceiver: PeerControllerTransceiver, upgrade_mode: UpgradeModeDetails) -> Self {
Self {
transceiver,
upgrade_mode,
}
}
pub async fn available_bandwidth(&self, ip: IpAddr) -> Result<i64, MetadataError> {
self.transceiver.query_bandwidth(ip).await
fn upgrade_mode_bandwidth(&self, true_bandwidth: i64) -> i64 {
// if we're undergoing upgrade mode, we don't meter bandwidth,
// we simply return MAX of clients current bandwidth and minimum bandwidth before default
// client would have attempted to send new ticket (hopefully)
// the latter is to support older clients that will ignore `upgrade_mode` field in the response
// as they're not aware of its existence
max(DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD, true_bandwidth)
}
pub async fn available_bandwidth(&self, ip: IpAddr) -> Result<ResponseData, MetadataError> {
let upgrade_mode = self.upgrade_mode.enabled();
let true_bandwidth = self.transceiver.query_bandwidth(ip).await?;
let available_bandwidth = if upgrade_mode {
self.upgrade_mode_bandwidth(true_bandwidth)
} else {
true_bandwidth
};
Ok(ResponseData::AvailableBandwidth {
amount: available_bandwidth,
upgrade_mode,
})
}
// Top up with a credential and return the afterwards available bandwidth
pub async fn topup_bandwidth(
&self,
ip: IpAddr,
credential: CredentialSpendingData,
) -> Result<i64, MetadataError> {
self.transceiver
.topup_bandwidth(ip, Box::new(credential))
.await
claim: Box<BandwidthCredential>,
) -> Result<ResponseData, MetadataError> {
match *claim {
BandwidthCredential::ZkNym(zk_nym) => {
// if we got zk-nym, we just try to verify it
let available_bandwidth = self.transceiver.topup_bandwidth(ip, zk_nym).await?;
// however, we still follow the same upgrade-mode logic,
// so that the client would not attempt to needlessly send more credentials
let upgrade_mode = self.upgrade_mode.enabled();
let available_bandwidth = if upgrade_mode {
self.upgrade_mode_bandwidth(available_bandwidth)
} else {
available_bandwidth
};
Ok(ResponseData::TopUpBandwidth {
available_bandwidth,
upgrade_mode,
})
}
BandwidthCredential::UpgradeModeJWT { token } => {
// if we're already in the upgrade mode, don't bother validating the token
if self.upgrade_mode.enabled() {
let true_bandwidth = self.transceiver.query_bandwidth(ip).await?;
return Ok(ResponseData::TopUpBandwidth {
available_bandwidth: self.upgrade_mode_bandwidth(true_bandwidth),
upgrade_mode: true,
});
}
// if the token is valid, try to check if we're behind
// and have to update our internal state
self.upgrade_mode
.try_enable_via_received_jwt(token)
.await
.map_err(|err| MetadataError::JWTVerification {
message: err.to_string(),
})?;
// if we didn't return an error, it means token got accepted
// and we have transitioned into the upgrade mode
let true_bandwidth = self.transceiver.query_bandwidth(ip).await?;
Ok(ResponseData::TopUpBandwidth {
available_bandwidth: self.upgrade_mode_bandwidth(true_bandwidth),
upgrade_mode: true,
})
}
}
}
pub async fn upgrade_mode_check(
&self,
request: UpgradeModeCheckRequestType,
) -> Result<ResponseData, MetadataError> {
// if we're already in the upgrade mode - no need to do anything
if self.upgrade_mode.enabled() {
return Ok(ResponseData::UpgradeMode { upgrade_mode: true });
}
match request {
UpgradeModeCheckRequestType::UpgradeModeJwt { token } => {
self.upgrade_mode
.try_enable_via_received_jwt(token)
.await
.map_err(|err| MetadataError::JWTVerification {
message: err.to_string(),
})?;
}
}
// if we didn't return an error, it means token got accepted
// and we have transitioned into the upgrade mode
Ok(ResponseData::UpgradeMode { upgrade_mode: true })
}
}
@@ -9,8 +9,7 @@ use axum::{
};
use nym_http_api_common::{FormattedResponse, OutputParams};
use nym_wireguard_private_metadata_shared::{
AxumErrorResponse, AxumResult, Construct, Extract, Request, Response,
interface::{RequestData, ResponseData},
AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, interface::RequestData,
latest,
};
use tower_http::compression::CompressionLayer;
@@ -25,6 +24,15 @@ pub(crate) fn bandwidth_routes() -> Router<AppState> {
.layer(CompressionLayer::new())
}
pub(crate) fn network_routes() -> Router<AppState> {
Router::new()
.route(
"/upgrade-mode-check",
axum::routing::post(upgrade_mode_check),
)
.layer(CompressionLayer::new())
}
#[utoipa::path(
tag = "bandwidth",
get,
@@ -59,20 +67,17 @@ async fn available_bandwidth(
) -> AxumResult<FormattedResponse<Response>> {
let output = output.output.unwrap_or_default();
let (RequestData::AvailableBandwidth(_), version) =
let (RequestData::AvailableBandwidth, version) =
request.extract().map_err(AxumErrorResponse::bad_request)?
else {
return Err(AxumErrorResponse::bad_request("incorrect request type"));
};
let available_bandwidth = state
let available_bandwidth_response = state
.available_bandwidth(addr.ip())
.await
.map_err(AxumErrorResponse::bad_request)?;
let response = Response::construct(
ResponseData::AvailableBandwidth(available_bandwidth),
version,
)
.map_err(AxumErrorResponse::bad_request)?;
let response = Response::construct(available_bandwidth_response, version)
.map_err(AxumErrorResponse::bad_request)?;
Ok(output.to_response(response))
}
@@ -96,16 +101,49 @@ async fn topup_bandwidth(
) -> AxumResult<FormattedResponse<Response>> {
let output = output.output.unwrap_or_default();
let (RequestData::TopUpBandwidth(credential), version) =
let (RequestData::TopUpBandwidth { credential }, version) =
request.extract().map_err(AxumErrorResponse::bad_request)?
else {
return Err(AxumErrorResponse::bad_request("incorrect request type"));
};
let available_bandwidth = state
.topup_bandwidth(addr.ip(), *credential)
let top_up_bandwidth_response = state
.topup_bandwidth(addr.ip(), credential)
.await
.map_err(AxumErrorResponse::bad_request)?;
let response = Response::construct(ResponseData::TopUpBandwidth(available_bandwidth), version)
let response = Response::construct(top_up_bandwidth_response, version)
.map_err(AxumErrorResponse::bad_request)?;
Ok(output.to_response(response))
}
#[utoipa::path(
tag = "network",
post,
request_body = Request,
path = "/v1/network/upgrade-mode-check",
responses(
(status = 200, content(
(Response = "application/bincode")
))
),
)]
async fn upgrade_mode_check(
Query(output): Query<OutputParams>,
State(state): State<AppState>,
Json(request): Json<Request>,
) -> AxumResult<FormattedResponse<Response>> {
let output = output.output.unwrap_or_default();
let (RequestData::UpgradeModeCheck { typ }, version) =
request.extract().map_err(AxumErrorResponse::bad_request)?
else {
return Err(AxumErrorResponse::bad_request("incorrect request type"));
};
let upgrade_mode_check_response = state
.upgrade_mode_check(typ)
.await
.map_err(AxumErrorResponse::bad_request)?;
let response = Response::construct(upgrade_mode_check_response, version)
.map_err(AxumErrorResponse::bad_request)?;
Ok(output.to_response(response))

Some files were not shown because too many files have changed in this diff Show More