diff --git a/Cargo.lock b/Cargo.lock index bbff2760be3..c3579b935ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5257,8 +5257,8 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "cosmrs", "nym-crypto", + "nym-gateway-client", "nym-gateway-requests", "serde", "sqlx", diff --git a/common/client-core/gateways-storage/.sqlx/query-06e743d143fcc4be20ca2af5e99b19f15d22fff72490473587a14cdc046fda32.json b/common/client-core/gateways-storage/.sqlx/query-06e743d143fcc4be20ca2af5e99b19f15d22fff72490473587a14cdc046fda32.json index 627c54341cf..20c47048d8d 100644 --- a/common/client-core/gateways-storage/.sqlx/query-06e743d143fcc4be20ca2af5e99b19f15d22fff72490473587a14cdc046fda32.json +++ b/common/client-core/gateways-storage/.sqlx/query-06e743d143fcc4be20ca2af5e99b19f15d22fff72490473587a14cdc046fda32.json @@ -6,14 +6,14 @@ { "name": "exists", "ordinal": 0, - "type_info": "Int" + "type_info": "Integer" } ], "parameters": { "Right": 1 }, "nullable": [ - null + false ] }, "hash": "06e743d143fcc4be20ca2af5e99b19f15d22fff72490473587a14cdc046fda32" diff --git a/common/client-core/gateways-storage/.sqlx/query-0e85ec18da67cf4e3df04ad80136571f6e920eb2290f20b1b8c5b0ab4b489985.json b/common/client-core/gateways-storage/.sqlx/query-0e85ec18da67cf4e3df04ad80136571f6e920eb2290f20b1b8c5b0ab4b489985.json deleted file mode 100644 index 61aa2489465..00000000000 --- a/common/client-core/gateways-storage/.sqlx/query-0e85ec18da67cf4e3df04ad80136571f6e920eb2290f20b1b8c5b0ab4b489985.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT * FROM remote_gateway_details WHERE gateway_id_bs58 = ?", - "describe": { - "columns": [ - { - "name": "gateway_id_bs58", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "gateway_owner_address", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "gateway_listener", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "derived_aes128_ctr_blake3_hmac_keys_bs58", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "derived_aes256_gcm_siv_key", - "ordinal": 4, - "type_info": "Blob" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - true, - false, - true, - true - ] - }, - "hash": "0e85ec18da67cf4e3df04ad80136571f6e920eb2290f20b1b8c5b0ab4b489985" -} diff --git a/common/client-core/gateways-storage/.sqlx/query-0f1dfb89f1eb39f4a58787af0f53a7a93afb7e4d2e54e2d38fd79d31c8575a54.json b/common/client-core/gateways-storage/.sqlx/query-0f1dfb89f1eb39f4a58787af0f53a7a93afb7e4d2e54e2d38fd79d31c8575a54.json deleted file mode 100644 index 7e5c8bf6fe6..00000000000 --- a/common/client-core/gateways-storage/.sqlx/query-0f1dfb89f1eb39f4a58787af0f53a7a93afb7e4d2e54e2d38fd79d31c8575a54.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE remote_gateway_details\n SET\n derived_aes128_ctr_blake3_hmac_keys_bs58 = ?,\n derived_aes256_gcm_siv_key = ?\n WHERE gateway_id_bs58 = ?\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "0f1dfb89f1eb39f4a58787af0f53a7a93afb7e4d2e54e2d38fd79d31c8575a54" -} diff --git a/common/client-core/gateways-storage/.sqlx/query-b059bc3688b6b7f83f47048db9897720fd4e6f3211bf74030a9638f7bf6738e4.json b/common/client-core/gateways-storage/.sqlx/query-2c113b37864f9fec7e64c0f8fdd38edcdf149acfd38c56a4db3bbf97bdb13210.json similarity index 56% rename from common/client-core/gateways-storage/.sqlx/query-b059bc3688b6b7f83f47048db9897720fd4e6f3211bf74030a9638f7bf6738e4.json rename to common/client-core/gateways-storage/.sqlx/query-2c113b37864f9fec7e64c0f8fdd38edcdf149acfd38c56a4db3bbf97bdb13210.json index 646b32adb84..4fd84fdf939 100644 --- a/common/client-core/gateways-storage/.sqlx/query-b059bc3688b6b7f83f47048db9897720fd4e6f3211bf74030a9638f7bf6738e4.json +++ b/common/client-core/gateways-storage/.sqlx/query-2c113b37864f9fec7e64c0f8fdd38edcdf149acfd38c56a4db3bbf97bdb13210.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n INSERT INTO custom_gateway_details(gateway_id_bs58, data) \n VALUES (?, ?)\n ", + "query": "\n INSERT INTO custom_gateway_details(gateway_id_bs58, data)\n VALUES (?, ?)\n ", "describe": { "columns": [], "parameters": { @@ -8,5 +8,5 @@ }, "nullable": [] }, - "hash": "b059bc3688b6b7f83f47048db9897720fd4e6f3211bf74030a9638f7bf6738e4" + "hash": "2c113b37864f9fec7e64c0f8fdd38edcdf149acfd38c56a4db3bbf97bdb13210" } diff --git a/common/client-core/gateways-storage/.sqlx/query-4b739e12ea8d917cb17580337caeabb05f0e3ddbec04fdfa111d0fc86ba75505.json b/common/client-core/gateways-storage/.sqlx/query-4b739e12ea8d917cb17580337caeabb05f0e3ddbec04fdfa111d0fc86ba75505.json new file mode 100644 index 00000000000..cade1517522 --- /dev/null +++ b/common/client-core/gateways-storage/.sqlx/query-4b739e12ea8d917cb17580337caeabb05f0e3ddbec04fdfa111d0fc86ba75505.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n rgd.gateway_id_bs58,\n derived_aes256_gcm_siv_key,\n gateway_listener,\n fallback_listener\n FROM\n remote_gateway_details AS rgd\n INNER JOIN\n remote_gateway_shared_keys AS rgsk\n ON\n rgd.gateway_id_bs58 = rgsk.gateway_id_bs58\n WHERE\n rgd.gateway_id_bs58 = ?", + "describe": { + "columns": [ + { + "name": "gateway_id_bs58", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "derived_aes256_gcm_siv_key", + "ordinal": 1, + "type_info": "Blob" + }, + { + "name": "gateway_listener", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "fallback_listener", + "ordinal": 3, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + true + ] + }, + "hash": "4b739e12ea8d917cb17580337caeabb05f0e3ddbec04fdfa111d0fc86ba75505" +} diff --git a/common/client-core/gateways-storage/.sqlx/query-700a75acbcd90c74baa7823c40739a8ff8a26400c1d2bd45a689970bf1ba0e66.json b/common/client-core/gateways-storage/.sqlx/query-700a75acbcd90c74baa7823c40739a8ff8a26400c1d2bd45a689970bf1ba0e66.json new file mode 100644 index 00000000000..ea42659a847 --- /dev/null +++ b/common/client-core/gateways-storage/.sqlx/query-700a75acbcd90c74baa7823c40739a8ff8a26400c1d2bd45a689970bf1ba0e66.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO remote_gateway_shared_keys(gateway_id_bs58, derived_aes256_gcm_siv_key)\n VALUES (?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "700a75acbcd90c74baa7823c40739a8ff8a26400c1d2bd45a689970bf1ba0e66" +} diff --git a/common/client-core/gateways-storage/.sqlx/query-8909fd329e7e5fb16c4989b15b3d3a12bba1569520e01f6f074178e23d6ee89e.json b/common/client-core/gateways-storage/.sqlx/query-727598e516090da6d26e36d09062b60ccb76d6468f359891428c0bfb96ddd7ef.json similarity index 51% rename from common/client-core/gateways-storage/.sqlx/query-8909fd329e7e5fb16c4989b15b3d3a12bba1569520e01f6f074178e23d6ee89e.json rename to common/client-core/gateways-storage/.sqlx/query-727598e516090da6d26e36d09062b60ccb76d6468f359891428c0bfb96ddd7ef.json index eabf0e5fb9f..389d07c6eae 100644 --- a/common/client-core/gateways-storage/.sqlx/query-8909fd329e7e5fb16c4989b15b3d3a12bba1569520e01f6f074178e23d6ee89e.json +++ b/common/client-core/gateways-storage/.sqlx/query-727598e516090da6d26e36d09062b60ccb76d6468f359891428c0bfb96ddd7ef.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n INSERT INTO registered_gateway(gateway_id_bs58, registration_timestamp, gateway_type) \n VALUES (?, ?, ?)\n ", + "query": "\n INSERT INTO registered_gateway(gateway_id_bs58, registration_timestamp, gateway_type)\n VALUES (?, ?, ?)\n ", "describe": { "columns": [], "parameters": { @@ -8,5 +8,5 @@ }, "nullable": [] }, - "hash": "8909fd329e7e5fb16c4989b15b3d3a12bba1569520e01f6f074178e23d6ee89e" + "hash": "727598e516090da6d26e36d09062b60ccb76d6468f359891428c0bfb96ddd7ef" } diff --git a/common/client-core/gateways-storage/.sqlx/query-a64a557ba87d4b2c7457857afa7ebc7d4f895fc4991da18ec02c9e250bea0fe0.json b/common/client-core/gateways-storage/.sqlx/query-a64a557ba87d4b2c7457857afa7ebc7d4f895fc4991da18ec02c9e250bea0fe0.json new file mode 100644 index 00000000000..38babea0bc0 --- /dev/null +++ b/common/client-core/gateways-storage/.sqlx/query-a64a557ba87d4b2c7457857afa7ebc7d4f895fc4991da18ec02c9e250bea0fe0.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO remote_gateway_details(gateway_id_bs58, gateway_listener, fallback_listener)\n VALUES (?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "a64a557ba87d4b2c7457857afa7ebc7d4f895fc4991da18ec02c9e250bea0fe0" +} diff --git a/common/client-core/gateways-storage/.sqlx/query-a6939bea03b10cde810a9a099bd597b4f51092e30a41c4085a8f8668f039f7c0.json b/common/client-core/gateways-storage/.sqlx/query-a6939bea03b10cde810a9a099bd597b4f51092e30a41c4085a8f8668f039f7c0.json deleted file mode 100644 index b2e77df70a9..00000000000 --- a/common/client-core/gateways-storage/.sqlx/query-a6939bea03b10cde810a9a099bd597b4f51092e30a41c4085a8f8668f039f7c0.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO remote_gateway_details(gateway_id_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key, gateway_owner_address, gateway_listener)\n VALUES (?, ?, ?, ?, ?)\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 5 - }, - "nullable": [] - }, - "hash": "a6939bea03b10cde810a9a099bd597b4f51092e30a41c4085a8f8668f039f7c0" -} diff --git a/common/client-core/gateways-storage/Cargo.toml b/common/client-core/gateways-storage/Cargo.toml index 938a6db5dc3..dc25a2b7486 100644 --- a/common/client-core/gateways-storage/Cargo.toml +++ b/common/client-core/gateways-storage/Cargo.toml @@ -9,7 +9,6 @@ rust-version.workspace = true [dependencies] async-trait.workspace = true -cosmrs.workspace = true serde = { workspace = true, features = ["derive"] } thiserror.workspace = true time.workspace = true @@ -20,6 +19,7 @@ zeroize = { workspace = true, features = ["zeroize_derive"] } nym-crypto = { path = "../../crypto", features = ["asymmetric"] } nym-gateway-requests = { path = "../../gateway-requests" } +nym-gateway-client = { path = "../../client-libs/gateway-client" } [target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] workspace = true diff --git a/common/client-core/gateways-storage/fs_gateways_migrations/20251126120000_remove_legacy_add_fallback.sql b/common/client-core/gateways-storage/fs_gateways_migrations/20251126120000_remove_legacy_add_fallback.sql new file mode 100644 index 00000000000..8073e41cf20 --- /dev/null +++ b/common/client-core/gateways-storage/fs_gateways_migrations/20251126120000_remove_legacy_add_fallback.sql @@ -0,0 +1,22 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + +CREATE TABLE remote_gateway_details_temp +( + gateway_id_bs58 TEXT NOT NULL UNIQUE PRIMARY KEY REFERENCES registered_gateway (gateway_id_bs58), + derived_aes256_gcm_siv_key BLOB NOT NULL, + gateway_listener TEXT NOT NULL, + fallback_listener TEXT, + expiration_timestamp DATETIME NOT NULL +); + +-- keep only registrations with a non null aes256 key +INSERT INTO remote_gateway_details_temp SELECT gateway_id_bs58, derived_aes256_gcm_siv_key, gateway_listener, NULL, datetime(0, 'unixepoch') FROM remote_gateway_details WHERE derived_aes256_gcm_siv_key IS NOT NULL; + +DROP TABLE remote_gateway_details; +ALTER TABLE remote_gateway_details_temp RENAME TO remote_gateway_details; + +-- delete others +DELETE FROM registered_gateway WHERE gateway_id_bs58 IN ( SELECT gateway_id_bs58 FROM remote_gateway_details WHERE derived_aes256_gcm_siv_key IS NULL); diff --git a/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs b/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs index 65537668574..38f1d2e4057 100644 --- a/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs +++ b/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs @@ -6,6 +6,7 @@ use crate::{ types::{ RawActiveGateway, RawCustomGatewayDetails, RawRegisteredGateway, RawRemoteGatewayDetails, }, + RawGatewayPublishedData, }; use sqlx::{ sqlite::{SqliteAutoVacuum, SqliteSynchronous}, @@ -144,13 +145,11 @@ impl StorageManager { &self, gateway_id: &str, ) -> Result { - sqlx::query_as!( - RawRemoteGatewayDetails, - "SELECT * FROM remote_gateway_details WHERE gateway_id_bs58 = ?", - gateway_id - ) - .fetch_one(&self.connection_pool) - .await + // query_as! macro doesn't use fromRow + sqlx::query_as("SELECT * FROM remote_gateway_details WHERE gateway_id_bs58 = ?") + .bind(gateway_id) + .fetch_one(&self.connection_pool) + .await } pub(crate) async fn set_remote_gateway_details( @@ -159,41 +158,36 @@ impl StorageManager { ) -> Result<(), sqlx::Error> { sqlx::query!( r#" - INSERT INTO remote_gateway_details(gateway_id_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key, gateway_owner_address, gateway_listener) + INSERT INTO remote_gateway_details(gateway_id_bs58, derived_aes256_gcm_siv_key, gateway_listener, fallback_listener, expiration_timestamp) VALUES (?, ?, ?, ?, ?) "#, remote.gateway_id_bs58, - remote.derived_aes128_ctr_blake3_hmac_keys_bs58, remote.derived_aes256_gcm_siv_key, - remote.gateway_owner_address, - remote.gateway_listener, + remote.published_data.gateway_listener, + remote.published_data.fallback_listener, + remote.published_data.expiration_timestamp ) .execute(&self.connection_pool) .await?; Ok(()) } - pub(crate) async fn update_remote_gateway_key( + pub(crate) async fn update_remote_gateway_published_data( &self, gateway_id_bs58: &str, - derived_aes128_ctr_blake3_hmac_keys_bs58: Option<&str>, - derived_aes256_gcm_siv_key: Option<&[u8]>, + published_data: &RawGatewayPublishedData, ) -> Result<(), sqlx::Error> { sqlx::query!( r#" - UPDATE remote_gateway_details - SET - derived_aes128_ctr_blake3_hmac_keys_bs58 = ?, - derived_aes256_gcm_siv_key = ? - WHERE gateway_id_bs58 = ? + UPDATE remote_gateway_details SET gateway_listener = ?, fallback_listener = ?, expiration_timestamp = ? WHERE gateway_id_bs58 = ? "#, - derived_aes128_ctr_blake3_hmac_keys_bs58, - derived_aes256_gcm_siv_key, + published_data.gateway_listener, + published_data.fallback_listener, + published_data.expiration_timestamp, gateway_id_bs58 ) - .execute(&self.connection_pool) - .await?; - + .execute(&self.connection_pool) + .await?; Ok(()) } diff --git a/common/client-core/gateways-storage/src/backend/fs_backend/mod.rs b/common/client-core/gateways-storage/src/backend/fs_backend/mod.rs index db89a3b83fa..a0dda2330ef 100644 --- a/common/client-core/gateways-storage/src/backend/fs_backend/mod.rs +++ b/common/client-core/gateways-storage/src/backend/fs_backend/mod.rs @@ -2,18 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ - ActiveGateway, BadGateway, GatewayDetails, GatewayRegistration, GatewayType, - GatewaysDetailsStore, StorageError, + ActiveGateway, BadGateway, GatewayDetails, GatewayPublishedData, GatewayRegistration, + GatewayType, GatewaysDetailsStore, StorageError, }; use async_trait::async_trait; use manager::StorageManager; use nym_crypto::asymmetric::ed25519; -use nym_gateway_requests::SharedSymmetricKey; use std::path::Path; pub mod error; mod manager; -mod models; #[derive(Clone)] pub struct OnDiskGatewaysDetails { @@ -134,17 +132,13 @@ impl GatewaysDetailsStore for OnDiskGatewaysDetails { Ok(()) } - async fn upgrade_stored_remote_gateway_key( + async fn update_gateway_published_data( &self, - gateway_id: ed25519::PublicKey, - updated_key: &SharedSymmetricKey, + gateway_id: &str, + published_data: &GatewayPublishedData, ) -> Result<(), Self::StorageError> { self.manager - .update_remote_gateway_key( - &gateway_id.to_base58_string(), - None, - Some(updated_key.as_bytes()), - ) + .update_remote_gateway_published_data(gateway_id, &published_data.into()) .await?; Ok(()) } diff --git a/common/client-core/gateways-storage/src/backend/fs_backend/models.rs b/common/client-core/gateways-storage/src/backend/fs_backend/models.rs deleted file mode 100644 index 755fb6cc8b8..00000000000 --- a/common/client-core/gateways-storage/src/backend/fs_backend/models.rs +++ /dev/null @@ -1,2 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 diff --git a/common/client-core/gateways-storage/src/backend/mem_backend.rs b/common/client-core/gateways-storage/src/backend/mem_backend.rs index 48235aaf1ff..34249dda3ba 100644 --- a/common/client-core/gateways-storage/src/backend/mem_backend.rs +++ b/common/client-core/gateways-storage/src/backend/mem_backend.rs @@ -2,10 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::types::{ActiveGateway, GatewayRegistration}; -use crate::{BadGateway, GatewayDetails, GatewaysDetailsStore}; +use crate::{BadGateway, GatewayDetails, GatewayPublishedData, GatewaysDetailsStore}; use async_trait::async_trait; -use nym_crypto::asymmetric::ed25519::PublicKey; -use nym_gateway_requests::{SharedGatewayKey, SharedSymmetricKey}; use std::collections::HashMap; use std::sync::Arc; use thiserror::Error; @@ -96,26 +94,17 @@ impl GatewaysDetailsStore for InMemGatewaysDetails { Ok(()) } - async fn upgrade_stored_remote_gateway_key( + async fn update_gateway_published_data( &self, - gateway_id: PublicKey, - updated_key: &SharedSymmetricKey, + gateway_id: &str, + published_data: &GatewayPublishedData, ) -> Result<(), Self::StorageError> { let mut guard = self.inner.write().await; - - #[allow(clippy::unwrap_used)] - if let Some(target) = guard.gateways.get_mut(&gateway_id.to_string()) { - let GatewayDetails::Remote(details) = &mut target.details else { - return Ok(()); - }; - assert_eq!(Arc::strong_count(&details.shared_key), 1); - - // eh. that's nasty, but it's only ever used for ephemeral clients so should be fine for now... - details.shared_key = Arc::new(SharedGatewayKey::Current( - SharedSymmetricKey::try_from_bytes(updated_key.as_bytes()).unwrap(), - )) + if let Some(gateway) = guard.gateways.get_mut(gateway_id) { + if let GatewayDetails::Remote(ref mut remote_details) = gateway.details { + remote_details.published_data = published_data.clone(); + } } - Ok(()) } diff --git a/common/client-core/gateways-storage/src/error.rs b/common/client-core/gateways-storage/src/error.rs index 2c251f513db..3cbb4aa3ddc 100644 --- a/common/client-core/gateways-storage/src/error.rs +++ b/common/client-core/gateways-storage/src/error.rs @@ -18,16 +18,6 @@ pub enum BadGateway { source: Ed25519RecoveryError, }, - #[error("the account owner of gateway {gateway_id} ({raw_owner}) is malformed: {source}")] - MalformedGatewayOwnerAccountAddress { - gateway_id: String, - - raw_owner: String, - - #[source] - source: cosmrs::ErrorReport, - }, - #[error("the shared keys provided for gateway {gateway_id} are malformed: {source}")] MalformedSharedKeys { gateway_id: String, @@ -50,4 +40,12 @@ pub enum BadGateway { #[source] source: url::ParseError, }, + + #[error("the listening address ({raw_listener}) is malformed: {source}")] + MalformedListenerNoId { + raw_listener: String, + + #[source] + source: url::ParseError, + }, } diff --git a/common/client-core/gateways-storage/src/lib.rs b/common/client-core/gateways-storage/src/lib.rs index fd3f1af1e36..08c077e74b9 100644 --- a/common/client-core/gateways-storage/src/lib.rs +++ b/common/client-core/gateways-storage/src/lib.rs @@ -6,7 +6,6 @@ use async_trait::async_trait; use nym_crypto::asymmetric::ed25519; -use nym_gateway_requests::SharedSymmetricKey; use std::error::Error; pub mod backend; @@ -60,10 +59,11 @@ pub trait GatewaysDetailsStore { details: &GatewayRegistration, ) -> Result<(), Self::StorageError>; - async fn upgrade_stored_remote_gateway_key( + /// Update the gateway details + async fn update_gateway_published_data( &self, - gateway_id: ed25519::PublicKey, - updated_key: &SharedSymmetricKey, + gateway_id: &str, + published_data: &GatewayPublishedData, ) -> Result<(), Self::StorageError>; /// Remove given gateway details from the underlying store. diff --git a/common/client-core/gateways-storage/src/types.rs b/common/client-core/gateways-storage/src/types.rs index 477e9df99f3..9b7c929a271 100644 --- a/common/client-core/gateways-storage/src/types.rs +++ b/common/client-core/gateways-storage/src/types.rs @@ -2,20 +2,21 @@ // SPDX-License-Identifier: Apache-2.0 use crate::BadGateway; -use cosmrs::AccountId; use nym_crypto::asymmetric::ed25519; -use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey}; +use nym_gateway_client::client::GatewayListeners; +use nym_gateway_requests::shared_key::SharedSymmetricKey; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; -use std::ops::Deref; use std::str::FromStr; use std::sync::Arc; +use time::Duration; use time::OffsetDateTime; use url::Url; use zeroize::{Zeroize, ZeroizeOnDrop}; pub const REMOTE_GATEWAY_TYPE: &str = "remote"; pub const CUSTOM_GATEWAY_TYPE: &str = "custom"; +const GATEWAY_DETAILS_TTL: Duration = Duration::days(7); #[derive(Debug, Clone, Default)] pub struct ActiveGateway { @@ -65,15 +66,13 @@ impl From for GatewayRegistration { impl GatewayDetails { pub fn new_remote( gateway_id: ed25519::PublicKey, - shared_key: Arc, - gateway_owner_address: Option, - gateway_listener: Url, + shared_key: Arc, + published_data: GatewayPublishedData, ) -> Self { GatewayDetails::Remote(RemoteGatewayDetails { gateway_id, shared_key, - gateway_owner_address, - gateway_listener, + published_data, }) } @@ -88,13 +87,20 @@ impl GatewayDetails { } } - pub fn shared_key(&self) -> Option<&SharedGatewayKey> { + pub fn shared_key(&self) -> Option<&SharedSymmetricKey> { match self { GatewayDetails::Remote(details) => Some(&details.shared_key), GatewayDetails::Custom(_) => None, } } + pub fn details_exipration(&self) -> Option { + match self { + GatewayDetails::Remote(details) => Some(details.published_data.expiration_timestamp), + GatewayDetails::Custom(_) => None, + } + } + pub fn is_custom(&self) -> bool { matches!(self, GatewayDetails::Custom(..)) } @@ -164,14 +170,78 @@ pub struct RegisteredGateway { pub gateway_type: GatewayType, } +#[derive(Debug, Clone)] +pub struct GatewayPublishedData { + pub listeners: GatewayListeners, + pub expiration_timestamp: OffsetDateTime, +} + +impl GatewayPublishedData { + pub fn new(listeners: GatewayListeners) -> GatewayPublishedData { + GatewayPublishedData { + listeners, + expiration_timestamp: OffsetDateTime::now_utc() + GATEWAY_DETAILS_TTL, + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +pub struct RawGatewayPublishedData { + pub gateway_listener: String, + pub fallback_listener: Option, + pub expiration_timestamp: OffsetDateTime, +} + +impl<'a> From<&'a GatewayPublishedData> for RawGatewayPublishedData { + fn from(value: &'a GatewayPublishedData) -> Self { + Self { + gateway_listener: value.listeners.primary.to_string(), + fallback_listener: value.listeners.fallback.as_ref().map(|uri| uri.to_string()), + expiration_timestamp: value.expiration_timestamp, + } + } +} + +impl TryFrom for GatewayPublishedData { + type Error = BadGateway; + + fn try_from(value: RawGatewayPublishedData) -> Result { + let gateway_listener: Url = Url::parse(&value.gateway_listener).map_err(|source| { + BadGateway::MalformedListenerNoId { + raw_listener: value.gateway_listener.clone(), + source, + } + })?; + let fallback_listener = value + .fallback_listener + .as_ref() + .map(|uri| { + Url::parse(uri).map_err(|source| BadGateway::MalformedListenerNoId { + raw_listener: uri.to_owned(), + source, + }) + }) + .transpose()?; + + Ok(GatewayPublishedData { + listeners: GatewayListeners { + primary: gateway_listener, + fallback: fallback_listener, + }, + expiration_timestamp: value.expiration_timestamp, + }) + } +} + #[derive(Debug, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] pub struct RawRemoteGatewayDetails { pub gateway_id_bs58: String, - pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option, - pub derived_aes256_gcm_siv_key: Option>, - pub gateway_owner_address: Option, - pub gateway_listener: String, + pub derived_aes256_gcm_siv_key: Vec, + #[zeroize(skip)] + #[cfg_attr(feature = "sqlx", sqlx(flatten))] + pub published_data: RawGatewayPublishedData, } impl TryFrom for RemoteGatewayDetails { @@ -186,81 +256,26 @@ impl TryFrom for RemoteGatewayDetails { } })?; - let shared_key = - match ( - &value.derived_aes256_gcm_siv_key, - &value.derived_aes128_ctr_blake3_hmac_keys_bs58, - ) { - (None, None) => { - return Err(BadGateway::MissingSharedKey { - gateway_id: value.gateway_id_bs58.clone(), - }) - } - (Some(aes256gcm_siv), _) => { - let current_key = - SharedSymmetricKey::try_from_bytes(aes256gcm_siv).map_err(|source| { - BadGateway::MalformedSharedKeys { - gateway_id: value.gateway_id_bs58.clone(), - source, - } - })?; - SharedGatewayKey::Current(current_key) - } - (None, Some(aes128ctr_hmac)) => { - let legacy_key = LegacySharedKeys::try_from_base58_string(aes128ctr_hmac) - .map_err(|source| BadGateway::MalformedSharedKeys { - gateway_id: value.gateway_id_bs58.clone(), - source, - })?; - SharedGatewayKey::Legacy(legacy_key) - } - }; - - let gateway_owner_address = value - .gateway_owner_address - .as_ref() - .map(|raw_owner| { - AccountId::from_str(raw_owner).map_err(|source| { - BadGateway::MalformedGatewayOwnerAccountAddress { - gateway_id: value.gateway_id_bs58.clone(), - raw_owner: raw_owner.clone(), - source, - } - }) - }) - .transpose()?; - - let gateway_listener = Url::parse(&value.gateway_listener).map_err(|source| { - BadGateway::MalformedListener { + let shared_key = SharedSymmetricKey::try_from_bytes(&value.derived_aes256_gcm_siv_key) + .map_err(|source| BadGateway::MalformedSharedKeys { gateway_id: value.gateway_id_bs58.clone(), - raw_listener: value.gateway_listener.clone(), source, - } - })?; + })?; Ok(RemoteGatewayDetails { gateway_id, shared_key: Arc::new(shared_key), - gateway_owner_address, - gateway_listener, + published_data: value.published_data.clone().try_into()?, }) } } impl<'a> From<&'a RemoteGatewayDetails> for RawRemoteGatewayDetails { fn from(value: &'a RemoteGatewayDetails) -> Self { - let (derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key) = - match value.shared_key.deref() { - SharedGatewayKey::Current(key) => (None, Some(key.to_bytes())), - SharedGatewayKey::Legacy(key) => (Some(key.to_base58_string()), None), - }; - RawRemoteGatewayDetails { gateway_id_bs58: value.gateway_id.to_base58_string(), - derived_aes128_ctr_blake3_hmac_keys_bs58, - derived_aes256_gcm_siv_key, - gateway_owner_address: value.gateway_owner_address.as_ref().map(|o| o.to_string()), - gateway_listener: value.gateway_listener.to_string(), + derived_aes256_gcm_siv_key: value.shared_key.to_bytes(), + published_data: (&value.published_data).into(), } } } @@ -269,11 +284,9 @@ impl<'a> From<&'a RemoteGatewayDetails> for RawRemoteGatewayDetails { pub struct RemoteGatewayDetails { pub gateway_id: ed25519::PublicKey, - pub shared_key: Arc, - - pub gateway_owner_address: Option, + pub shared_key: Arc, - pub gateway_listener: Url, + pub published_data: GatewayPublishedData, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/common/client-core/src/cli_helpers/client_add_gateway.rs b/common/client-core/src/cli_helpers/client_add_gateway.rs index dc4280ba296..5a1950a35aa 100644 --- a/common/client-core/src/cli_helpers/client_add_gateway.rs +++ b/common/client-core/src/cli_helpers/client_add_gateway.rs @@ -87,6 +87,7 @@ where user_chosen_gateway_id.map(|id| id.to_base58_string()), Some(common_args.latency_based_selection), common_args.force_tls_gateway, + false, ); tracing::debug!("Gateway selection specification: {selection_spec:?}"); @@ -167,6 +168,7 @@ where identity: gateway_details.gateway_id, active: common_args.set_active, typ: gateway_registration.details.typ().to_string(), - endpoint: Some(gateway_details.gateway_listener.clone()), + endpoint: Some(gateway_details.published_data.listeners.primary.clone()), + fallback_endpoint: gateway_details.published_data.listeners.fallback.clone(), }) } diff --git a/common/client-core/src/cli_helpers/client_init.rs b/common/client-core/src/cli_helpers/client_init.rs index 57f800c8cf7..8f7320a14be 100644 --- a/common/client-core/src/cli_helpers/client_init.rs +++ b/common/client-core/src/cli_helpers/client_init.rs @@ -140,6 +140,7 @@ where user_chosen_gateway_id.map(|id| id.to_base58_string()), Some(common_args.latency_based_selection), common_args.force_tls_gateway, + false, ); tracing::debug!("Gateway selection specification: {selection_spec:?}"); diff --git a/common/client-core/src/cli_helpers/client_list_gateways.rs b/common/client-core/src/cli_helpers/client_list_gateways.rs index 34259df02d7..e0c3d0ba9ee 100644 --- a/common/client-core/src/cli_helpers/client_list_gateways.rs +++ b/common/client-core/src/cli_helpers/client_list_gateways.rs @@ -56,7 +56,8 @@ where identity: remote_details.gateway_id, active: active_gateway == Some(remote_details.gateway_id), typ: GatewayType::Remote.to_string(), - endpoint: Some(remote_details.gateway_listener), + endpoint: Some(remote_details.published_data.listeners.primary.clone()), + fallback_endpoint: remote_details.published_data.listeners.fallback.clone(), }), GatewayDetails::Custom(_) => info.push(GatewayInfo { registration: gateway.registration_timestamp, @@ -64,6 +65,7 @@ where active: active_gateway == Some(gateway.details.gateway_id()), typ: gateway.details.typ().to_string(), endpoint: None, + fallback_endpoint: None, }), }; } diff --git a/common/client-core/src/cli_helpers/types.rs b/common/client-core/src/cli_helpers/types.rs index 1cdc7316379..0ccf98829f0 100644 --- a/common/client-core/src/cli_helpers/types.rs +++ b/common/client-core/src/cli_helpers/types.rs @@ -15,6 +15,7 @@ pub struct GatewayInfo { pub typ: String, pub endpoint: Option, + pub fallback_endpoint: Option, } impl Display for GatewayInfo { @@ -30,6 +31,9 @@ impl Display for GatewayInfo { if let Some(endpoint) = &self.endpoint { write!(f, " endpoint: {endpoint}")?; } + if let Some(fallback_endpoint) = &self.fallback_endpoint { + write!(f, " fallback: {fallback_endpoint}")?; + } Ok(()) } } diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index e414e81ed5a..600085b4cf8 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -529,7 +529,6 @@ where config: &Config, initialisation_result: InitialisationResult, bandwidth_controller: Option>, - details_store: &S::GatewaysDetailsStore, packet_router: PacketRouter, stats_reporter: ClientStatsSender, #[cfg(unix)] connection_fd_callback: Option>, @@ -555,14 +554,7 @@ where shutdown_tracker.clone_shutdown_token(), ) } else { - let cfg = GatewayConfig::new( - details.gateway_id, - details - .gateway_owner_address - .as_ref() - .map(|o| o.to_string()), - details.gateway_listener.to_string(), - ); + let cfg = GatewayConfig::new(details.gateway_id, details.published_data.listeners); GatewayClient::new( GatewayClientConfig::new_default() .with_disabled_credentials_mode(config.client.disabled_credentials_mode) @@ -592,32 +584,13 @@ where // the gateway client startup procedure is slightly more complicated now // we need to: // - perform handshake (reg or auth) - // - check for key upgrade - // - maybe perform another upgrade handshake // - check for bandwidth // - start background tasks - let auth_res = gateway_client + let _ = gateway_client .perform_initial_authentication() .await .map_err(gateway_failure)?; - if auth_res.requires_key_upgrade { - // drop the shared_key arc because we don't need it and we can't hold it for the purposes of upgrade - drop(auth_res); - - let updated_key = gateway_client - .upgrade_key_authenticated() - .await - .map_err(gateway_failure)?; - - details_store - .upgrade_stored_remote_gateway_key(gateway_client.gateway_identity(), &updated_key) - .await.map_err(|err| { - tracing::error!("failed to store upgraded gateway key! this connection might be forever broken now: {err}"); - ClientCoreError::GatewaysDetailsStoreError { source: Box::new(err) } - })? - } - gateway_client .claim_initial_bandwidth() .await @@ -636,7 +609,6 @@ where config: &Config, initialisation_result: InitialisationResult, bandwidth_controller: Option>, - details_store: &S::GatewaysDetailsStore, packet_router: PacketRouter, stats_reporter: ClientStatsSender, #[cfg(unix)] connection_fd_callback: Option>, @@ -667,7 +639,6 @@ where config, initialisation_result, bandwidth_controller, - details_store, packet_router, stats_reporter, #[cfg(unix)] @@ -975,8 +946,7 @@ where ) .await?; - let (reply_storage_backend, credential_store, details_store) = - self.client_store.into_runtime_stores(); + let (reply_storage_backend, credential_store, _) = self.client_store.into_runtime_stores(); // channels for inter-component communication // TODO: make the channels be internally created by the relevant components @@ -1069,7 +1039,6 @@ where &self.config, init_res, bandwidth_controller, - &details_store, gateway_packet_router, stats_reporter.clone(), #[cfg(unix)] diff --git a/common/client-core/src/client/base_client/storage/helpers.rs b/common/client-core/src/client/base_client/storage/helpers.rs index ba4cb4b20af..5604ea68edc 100644 --- a/common/client-core/src/client/base_client/storage/helpers.rs +++ b/common/client-core/src/client/base_client/storage/helpers.rs @@ -4,7 +4,9 @@ use crate::client::key_manager::persistence::KeyStore; use crate::client::key_manager::ClientKeys; use crate::error::ClientCoreError; -use nym_client_core_gateways_storage::{ActiveGateway, GatewayRegistration, GatewaysDetailsStore}; +use nym_client_core_gateways_storage::{ + ActiveGateway, GatewayPublishedData, GatewayRegistration, GatewaysDetailsStore, +}; use nym_crypto::asymmetric::ed25519; // helpers for error wrapping @@ -85,6 +87,23 @@ where }) } +pub async fn update_stored_published_data_gateway( + details_store: &D, + gateway_id: &str, + published_data: &GatewayPublishedData, +) -> Result<(), ClientCoreError> +where + D: GatewaysDetailsStore, + D::StorageError: Send + Sync + 'static, +{ + details_store + .update_gateway_published_data(gateway_id, published_data) + .await + .map_err(|source| ClientCoreError::GatewaysDetailsStoreError { + source: Box::new(source), + }) +} + pub async fn load_active_gateway_details( details_store: &D, ) -> Result diff --git a/common/client-core/src/client/base_client/storage/migration_helpers.rs b/common/client-core/src/client/base_client/storage/migration_helpers.rs index 3ac62132f0f..b115b23f972 100644 --- a/common/client-core/src/client/base_client/storage/migration_helpers.rs +++ b/common/client-core/src/client/base_client/storage/migration_helpers.rs @@ -2,210 +2,18 @@ // SPDX-License-Identifier: Apache-2.0 pub mod v1_1_33 { - use crate::client::base_client::{ - non_wasm_helpers::setup_fs_gateways_storage, - storage::helpers::{set_active_gateway, store_gateway_details}, - }; use crate::config::disk_persistence::old_v1_1_33::CommonClientPathsV1_1_33; use crate::config::disk_persistence::CommonClientPaths; use crate::config::old_config_v1_1_33::OldGatewayEndpointConfigV1_1_33; use crate::error::ClientCoreError; - use nym_client_core_gateways_storage::{ - CustomGatewayDetails, GatewayDetails, GatewayRegistration, RemoteGatewayDetails, - }; - use nym_gateway_requests::shared_key::LegacySharedKeys; - use serde::{Deserialize, Serialize}; - use sha2::{digest::Digest, Sha256}; - use std::ops::Deref; - use std::path::Path; - use std::sync::Arc; - use zeroize::Zeroizing; - - mod base64 { - use base64::{engine::general_purpose::STANDARD, Engine as _}; - use serde::{Deserialize, Deserializer, Serializer}; - - pub fn serialize(bytes: &[u8], serializer: S) -> Result { - serializer.serialize_str(&STANDARD.encode(bytes)) - } - - pub fn deserialize<'de, D: Deserializer<'de>>( - deserializer: D, - ) -> Result, D::Error> { - let s = ::deserialize(deserializer)?; - STANDARD.decode(s).map_err(serde::de::Error::custom) - } - } - - #[derive(Debug, Clone, Serialize, Deserialize)] - #[serde(untagged)] - enum PersistedGatewayDetails { - /// Standard details of a remote gateway - Default(PersistedGatewayConfig), - - /// Custom gateway setup, such as for a client embedded inside gateway itself - Custom(PersistedCustomGatewayDetails), - } - - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] - struct PersistedGatewayConfig { - /// The hash of the shared keys to ensure the correct ones are used with those gateway details. - #[serde(with = "base64")] - key_hash: Vec, - - /// Actual gateway details being persisted. - details: OldGatewayEndpointConfigV1_1_33, - } - - impl PersistedGatewayConfig { - fn verify(&self, shared_key: &LegacySharedKeys) -> bool { - let key_bytes = Zeroizing::new(shared_key.to_bytes()); - - let mut key_hasher = Sha256::new(); - key_hasher.update(&key_bytes); - let key_hash = key_hasher.finalize(); - - self.key_hash == key_hash.deref() - } - } - - #[derive(Debug, Clone, Serialize, Deserialize)] - struct PersistedCustomGatewayDetails { - gateway_id: String, - } - - fn load_shared_key>(path: P) -> Result { - // the shared key was a simple pem file - Ok(nym_pemstore::load_key(path)?) - } - - fn gateway_details_from_raw( - gateway_id: String, - gateway_owner: String, - gateway_listener: String, - gateway_shared_key: LegacySharedKeys, - ) -> Result { - Ok(GatewayDetails::Remote(RemoteGatewayDetails { - gateway_id: gateway_id - .parse() - .map_err(|err| ClientCoreError::UpgradeFailure { - message: format!("the stored gateway id was malformed: {err}"), - })?, - shared_key: Arc::new(gateway_shared_key.into()), - gateway_owner_address: Some(gateway_owner.parse().map_err(|err| { - ClientCoreError::UpgradeFailure { - message: format!("the stored gateway owner address was malformed: {err}"), - } - })?), - gateway_listener: gateway_listener.parse().map_err(|err| { - ClientCoreError::UpgradeFailure { - message: format!("the stored gateway listener address was malformed: {err}"), - } - })?, - })) - } - - // helper to extract shared key and gateway details into the new GatewayRegistration - fn extract_gateway_registration( - storage_paths: &CommonClientPathsV1_1_33, - ) -> Result { - let details_file = std::fs::File::open(&storage_paths.gateway_details).map_err(|err| { - ClientCoreError::UpgradeFailure { - message: format!( - "failed to open gateway details file at {}: {err}", - storage_paths.gateway_details.display() - ), - } - })?; - - // in v1.1.33 of the clients, the gateway details struct was saved as json - let details: PersistedGatewayDetails = - serde_json::from_reader(details_file).map_err(|err| { - ClientCoreError::UpgradeFailure { - message: format!( - "failed to deserialize gateway details from {}: {err}", - storage_paths.gateway_details.display() - ), - } - })?; - - let details = match details { - PersistedGatewayDetails::Default(config) => { - let gateway_shared_key = - load_shared_key(&storage_paths.keys.gateway_shared_key_file)?; - if !config.verify(&gateway_shared_key) { - return Err(ClientCoreError::UpgradeFailure { - message: "failed to verify consistency of the existing gateway details" - .to_string(), - }); - } - gateway_details_from_raw( - config.details.gateway_id, - config.details.gateway_owner, - config.details.gateway_listener, - gateway_shared_key, - )? - } - PersistedGatewayDetails::Custom(custom) => { - GatewayDetails::Custom(CustomGatewayDetails { - gateway_id: custom.gateway_id.parse().map_err(|err| { - ClientCoreError::UpgradeFailure { - message: format!("the stored gateway id was malformed: {err}"), - } - })?, - data: None, - }) - } - }; - - Ok(details.into()) - } - - // it's responsibility of the caller to ensure this is called **after** new registration has already been saved - fn remove_old_gateway_details(storage_paths: &CommonClientPathsV1_1_33) -> std::io::Result<()> { - std::fs::remove_file(&storage_paths.gateway_details)?; - - if storage_paths.keys.gateway_shared_key_file.exists() { - std::fs::remove_file(&storage_paths.keys.gateway_shared_key_file)?; - } - Ok(()) - } pub async fn migrate_gateway_details( - old_storage_paths: &CommonClientPathsV1_1_33, - new_storage_paths: &CommonClientPaths, - preloaded_config: Option, + _old_storage_paths: &CommonClientPathsV1_1_33, + _new_storage_paths: &CommonClientPaths, + _preloaded_config: Option, ) -> Result<(), ClientCoreError> { - let gateway_registration = match preloaded_config { - Some(config) => { - let gateway_shared_key = - load_shared_key(&old_storage_paths.keys.gateway_shared_key_file)?; - gateway_details_from_raw( - config.gateway_id, - config.gateway_owner, - config.gateway_listener, - gateway_shared_key, - )? - .into() - } - None => extract_gateway_registration(old_storage_paths)?, - }; - - // since we're migrating to a brand new store, the store should be empty - // and thus set the 'new' gateway as the active one - let details_store = - setup_fs_gateways_storage(&new_storage_paths.gateway_registrations).await?; - store_gateway_details(&details_store, &gateway_registration).await?; - set_active_gateway( - &details_store, - &gateway_registration.details.gateway_id().to_base58_string(), - ) - .await?; - - remove_old_gateway_details(old_storage_paths).map_err(|err| { - ClientCoreError::UpgradeFailure { - message: format!("failed to remove old data: {err}"), - } - }) + Err(ClientCoreError::UnsupportedMigration( + "migration of legacy keys has been removed and is no longer supported".into(), + )) } } diff --git a/common/client-core/src/client/key_manager/mod.rs b/common/client-core/src/client/key_manager/mod.rs index 24d59432032..5af5f1f1ec3 100644 --- a/common/client-core/src/client/key_manager/mod.rs +++ b/common/client-core/src/client/key_manager/mod.rs @@ -6,7 +6,7 @@ use nym_crypto::{ asymmetric::{ed25519, x25519}, hkdf::{DerivationMaterial, InvalidLength}, }; -use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey}; +use nym_gateway_requests::shared_key::SharedSymmetricKey; use nym_sphinx::acknowledgements::AckKey; use rand::{CryptoRng, RngCore}; use std::sync::Arc; @@ -106,7 +106,5 @@ fn _assert_keys_zeroize_on_drop() { _assert_zeroize_on_drop::(); _assert_zeroize_on_drop::(); _assert_zeroize_on_drop::(); - _assert_zeroize_on_drop::(); _assert_zeroize_on_drop::(); - _assert_zeroize_on_drop::(); } diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index eea24910d02..44eca8ac0bb 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -16,6 +16,9 @@ use std::path::PathBuf; #[derive(thiserror::Error, Debug)] pub enum ClientCoreError { + #[error("could not perform the state migration: {0}")] + UnsupportedMigration(String), + #[error("I/O error: {0}")] IoError(#[from] std::io::Error), @@ -43,6 +46,9 @@ pub enum ClientCoreError { #[error("Invalid URL: {0}")] InvalidUrl(String), + #[error("node doesn't advertise ip addresses : {0}")] + MissingIpAddress(String), + #[cfg(not(target_arch = "wasm32"))] #[error("resolution failed: {0}")] ResolutionFailed(#[from] nym_http_api_client::ResolveError), @@ -163,6 +169,9 @@ pub enum ClientCoreError { #[error("custom selection of gateway was expected")] CustomGatewaySelectionExpected, + #[error("custom selection of gateway was unexpected")] + UnexpectedCustomGatewaySelection, + #[error("the persisted gateway details were set for a custom setup")] UnexpectedPersistedCustomGatewayDetails, diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index 72d310bfcc8..8cb926fe555 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -5,6 +5,7 @@ use crate::error::ClientCoreError; use crate::init::types::RegistrationResult; use futures::{SinkExt, StreamExt}; use nym_crypto::asymmetric::ed25519; +use nym_gateway_client::client::GatewayListeners; use nym_gateway_client::GatewayClient; use nym_topology::node::RoutingNode; use nym_validator_client::client::{IdentityKeyRef, NymApiClientExt}; @@ -379,12 +380,12 @@ pub(super) fn get_specified_gateway( pub(super) async fn register_with_gateway( gateway_id: ed25519::PublicKey, - gateway_listener: Url, + gateway_listeners: GatewayListeners, our_identity: Arc, #[cfg(unix)] connection_fd_callback: Option>, ) -> Result { let mut gateway_client = GatewayClient::new_init( - gateway_listener, + gateway_listeners, gateway_id, our_identity.clone(), #[cfg(unix)] @@ -409,14 +410,6 @@ pub(super) async fn register_with_gateway( } })?; - // this should NEVER happen, if it did, it means the function was misused, - // because for any fresh **registration**, the derived key is always up to date - if auth_response.requires_key_upgrade { - return Err(ClientCoreError::UnexpectedKeyUpgrade { - gateway_id: gateway_id.to_base58_string(), - }); - } - Ok(RegistrationResult { shared_keys: auth_response.initial_shared_key, authenticated_ephemeral_client: gateway_client, diff --git a/common/client-core/src/init/mod.rs b/common/client-core/src/init/mod.rs index 5bece573fdc..8c7b99c6e97 100644 --- a/common/client-core/src/init/mod.rs +++ b/common/client-core/src/init/mod.rs @@ -5,7 +5,7 @@ use crate::client::base_client::storage::helpers::{ has_gateway_details, load_active_gateway_details, load_client_keys, load_gateway_details, - store_gateway_details, + store_gateway_details, update_stored_published_data_gateway, }; use crate::client::key_manager::persistence::KeyStore; use crate::client::key_manager::ClientKeys; @@ -16,8 +16,8 @@ use crate::init::helpers::{ use crate::init::types::{ GatewaySelectionSpecification, GatewaySetup, InitialisationResult, SelectedGateway, }; -use nym_client_core_gateways_storage::GatewaysDetailsStore; use nym_client_core_gateways_storage::{GatewayDetails, GatewayRegistration}; +use nym_client_core_gateways_storage::{GatewayPublishedData, GatewaysDetailsStore}; use nym_gateway_client::client::InitGatewayClient; use nym_topology::node::RoutingNode; use rand::rngs::OsRng; @@ -71,21 +71,28 @@ where let mut rng = OsRng; let selected_gateway = match selection_specification { - GatewaySelectionSpecification::UniformRemote { must_use_tls } => { + GatewaySelectionSpecification::UniformRemote { + must_use_tls, + no_hostname, + } => { let gateway = uniformly_random_gateway(&mut rng, &available_gateways, must_use_tls)?; - SelectedGateway::from_topology_node(gateway, must_use_tls)? + SelectedGateway::from_topology_node(gateway, must_use_tls, no_hostname)? } - GatewaySelectionSpecification::RemoteByLatency { must_use_tls } => { + GatewaySelectionSpecification::RemoteByLatency { + must_use_tls, + no_hostname, + } => { let gateway = choose_gateway_by_latency(&mut rng, &available_gateways, must_use_tls).await?; - SelectedGateway::from_topology_node(gateway, must_use_tls)? + SelectedGateway::from_topology_node(gateway, must_use_tls, no_hostname)? } GatewaySelectionSpecification::Specified { must_use_tls, + no_hostname, identity, } => { let gateway = get_specified_gateway(&identity, &available_gateways, must_use_tls)?; - SelectedGateway::from_topology_node(gateway, must_use_tls)? + SelectedGateway::from_topology_node(gateway, must_use_tls, no_hostname)? } GatewaySelectionSpecification::Custom { gateway_identity, @@ -105,15 +112,15 @@ where let (gateway_details, authenticated_ephemeral_client) = match selected_gateway { SelectedGateway::Remote { gateway_id, - gateway_owner_address, - gateway_listener, + + gateway_listeners, } => { // if we're using a 'normal' gateway setup, do register let our_identity = client_keys.identity_keypair(); let registration = helpers::register_with_gateway( gateway_id, - gateway_listener.clone(), + gateway_listeners.clone(), our_identity, #[cfg(unix)] connection_fd_callback, @@ -123,8 +130,7 @@ where GatewayDetails::new_remote( gateway_id, registration.shared_keys, - gateway_owner_address, - gateway_listener, + GatewayPublishedData::new(gateway_listeners), ), Some(registration.authenticated_ephemeral_client), ) @@ -150,6 +156,41 @@ where }) } +pub async fn refresh_gateway_published_data( + details_store: &D, + registration: GatewayRegistration, + available_gateways: Vec, + must_use_tls: bool, + no_hostname: bool, +) -> Result<(), ClientCoreError> +where + D: GatewaysDetailsStore, + D::StorageError: Send + Sync + 'static, +{ + let gateway_id = registration.gateway_id().to_base58_string(); + tracing::trace!("Updating gateway details : {gateway_id}"); + + let gateway = get_specified_gateway(&gateway_id, &available_gateways, must_use_tls)?; + let selected_gateway = SelectedGateway::from_topology_node(gateway, must_use_tls, no_hostname)?; + + let new_gateway_listeners = match selected_gateway { + SelectedGateway::Remote { + gateway_listeners, .. + } => gateway_listeners, + SelectedGateway::Custom { .. } => { + // this should not happen, as `from_topology_node` returns a Remote + Err(ClientCoreError::UnexpectedCustomGatewaySelection)? + } + }; + + let new_published_data = GatewayPublishedData::new(new_gateway_listeners); + + // update gateway details + update_stored_published_data_gateway(details_store, &gateway_id, &new_published_data).await?; + + Ok(()) +} + async fn use_loaded_gateway_details( key_store: &K, details_store: &D, diff --git a/common/client-core/src/init/types.rs b/common/client-core/src/init/types.rs index 8f09980a43e..75ee6a6a311 100644 --- a/common/client-core/src/init/types.rs +++ b/common/client-core/src/init/types.rs @@ -10,12 +10,11 @@ use nym_client_core_gateways_storage::{ GatewayRegistration, GatewaysDetailsStore, RemoteGatewayDetails, }; use nym_crypto::asymmetric::ed25519; -use nym_gateway_client::client::InitGatewayClient; -use nym_gateway_requests::shared_key::SharedGatewayKey; +use nym_gateway_client::client::{GatewayListeners, InitGatewayClient}; +use nym_gateway_client::SharedSymmetricKey; use nym_sphinx::addressing::clients::Recipient; use nym_topology::node::RoutingNode; use nym_validator_client::client::IdentityKey; -use nym_validator_client::nyxd::AccountId; use serde::Serialize; use std::fmt::{Debug, Display}; #[cfg(unix)] @@ -28,9 +27,7 @@ pub enum SelectedGateway { Remote { gateway_id: ed25519::PublicKey, - gateway_owner_address: Option, - - gateway_listener: Url, + gateway_listeners: GatewayListeners, }, Custom { gateway_id: ed25519::PublicKey, @@ -42,24 +39,40 @@ impl SelectedGateway { pub fn from_topology_node( node: RoutingNode, must_use_tls: bool, + no_hostname: bool, ) -> Result { // for now, let's use 'old' behaviour, if you want to change it, you can pass it up the enum stack yourself : ) let prefer_ipv6 = false; - let gateway_listener = if must_use_tls { - node.ws_entry_address_tls() - .ok_or(ClientCoreError::UnsupportedWssProtocol { - gateway: node.identity_key.to_base58_string(), - })? + let (gateway_listener, fallback_listener) = if must_use_tls { + // WSS main, no fallback + let primary = + node.ws_entry_address_tls() + .ok_or(ClientCoreError::UnsupportedWssProtocol { + gateway: node.identity_key.to_base58_string(), + })?; + (primary, None) } else { - node.ws_entry_address(prefer_ipv6) - .ok_or(ClientCoreError::UnsupportedEntry { + let (maybe_primary, fallback) = + node.ws_entry_address_with_fallback(prefer_ipv6, no_hostname); + ( + maybe_primary.ok_or(ClientCoreError::UnsupportedEntry { id: node.node_id, identity: node.identity_key.to_base58_string(), - })? + })?, + fallback, + ) }; - let gateway_listener = + let fallback_listener_url = fallback_listener.and_then(|address| { + Url::parse(&address) + .inspect_err(|err| { + tracing::warn!("Malformed fallback listener, none will be used : {err}") + }) + .ok() + }); + + let gateway_listener_url = Url::parse(&gateway_listener).map_err(|source| ClientCoreError::MalformedListener { gateway_id: node.identity_key.to_base58_string(), raw_listener: gateway_listener, @@ -68,8 +81,10 @@ impl SelectedGateway { Ok(SelectedGateway::Remote { gateway_id: node.identity_key, - gateway_owner_address: None, - gateway_listener, + gateway_listeners: GatewayListeners { + primary: gateway_listener_url, + fallback: fallback_listener_url, + }, }) } @@ -98,7 +113,7 @@ impl SelectedGateway { /// - shared keys derived between ourselves and the node /// - an authenticated handle of an ephemeral handle created for the purposes of registration pub struct RegistrationResult { - pub shared_keys: Arc, + pub shared_keys: Arc, pub authenticated_ephemeral_client: InitGatewayClient, } @@ -145,20 +160,36 @@ impl InitialisationResult { pub fn gateway_id(&self) -> ed25519::PublicKey { self.gateway_registration.details.gateway_id() } + + // indicates if the remote gateway details TTL has expired + pub fn exipred_details(&self) -> bool { + if let Some(expiration_timestamp) = self.gateway_registration.details.details_exipration() { + OffsetDateTime::now_utc() > expiration_timestamp + } else { + false + } + } } #[derive(Clone, Debug)] pub enum GatewaySelectionSpecification { /// Uniformly choose a random remote gateway. - UniformRemote { must_use_tls: bool }, + UniformRemote { + must_use_tls: bool, + no_hostname: bool, + }, /// Should the new, remote, gateway be selected based on latency. - RemoteByLatency { must_use_tls: bool }, + RemoteByLatency { + must_use_tls: bool, + no_hostname: bool, + }, /// Gateway with this specific identity should be chosen. // JS: I don't really like the name of this enum variant but couldn't think of anything better at the time Specified { must_use_tls: bool, + no_hostname: bool, identity: IdentityKey, }, @@ -174,6 +205,7 @@ impl Default for GatewaySelectionSpecification { fn default() -> Self { GatewaySelectionSpecification::UniformRemote { must_use_tls: false, + no_hostname: false, } } } @@ -183,16 +215,24 @@ impl GatewaySelectionSpecification { gateway_identity: Option, latency_based_selection: Option, must_use_tls: bool, + no_hostname: bool, ) -> Self { if let Some(identity) = gateway_identity { GatewaySelectionSpecification::Specified { identity, must_use_tls, + no_hostname, } } else if let Some(true) = latency_based_selection { - GatewaySelectionSpecification::RemoteByLatency { must_use_tls } + GatewaySelectionSpecification::RemoteByLatency { + must_use_tls, + no_hostname, + } } else { - GatewaySelectionSpecification::UniformRemote { must_use_tls } + GatewaySelectionSpecification::UniformRemote { + must_use_tls, + no_hostname, + } } } } @@ -315,6 +355,7 @@ pub struct InitResults { pub encryption_key: String, pub gateway_id: String, pub gateway_listener: String, + pub fallback_listener: Option, pub gateway_registration: OffsetDateTime, pub address: Recipient, } @@ -332,7 +373,13 @@ impl InitResults { identity_key: address.identity().to_base58_string(), encryption_key: address.encryption_key().to_base58_string(), gateway_id: gateway.gateway_id.to_base58_string(), - gateway_listener: gateway.gateway_listener.to_string(), + gateway_listener: gateway.published_data.listeners.primary.to_string(), + fallback_listener: gateway + .published_data + .listeners + .fallback + .as_ref() + .map(|uri| uri.to_string()), gateway_registration: registration, address, } diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index e9d91beeb95..1d75b31835f 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -21,8 +21,8 @@ use nym_crypto::asymmetric::ed25519; use nym_gateway_requests::registration::handshake::client_handshake; use nym_gateway_requests::{ BandwidthResponse, BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersion, - GatewayProtocolVersionExt, GatewayRequestsError, SensitiveServerResponse, ServerResponse, - SharedGatewayKey, SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, + GatewayProtocolVersionExt, GatewayRequestsError, ServerResponse, SharedSymmetricKey, + CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION, }; use nym_sphinx::forwarding::packet::MixPacket; use nym_statistics_common::clients::connection::ConnectionStatsEvent; @@ -47,43 +47,39 @@ use std::os::raw::c_int as RawFd; use wasm_utils::websocket::JSWebsocket; #[cfg(target_arch = "wasm32")] use wasmtimer::tokio::sleep; -use zeroize::Zeroizing; pub mod config; #[cfg(not(target_arch = "wasm32"))] pub(crate) mod websockets; #[cfg(not(target_arch = "wasm32"))] -use websockets::connect_async; +use crate::client::websockets::connect_async_with_fallback; pub struct GatewayConfig { pub gateway_identity: ed25519::PublicKey, - // currently a dead field - pub gateway_owner: Option, - - pub gateway_listener: String, + pub gateway_listeners: GatewayListeners, } impl GatewayConfig { - pub fn new( - gateway_identity: ed25519::PublicKey, - gateway_owner: Option, - gateway_listener: String, - ) -> Self { + pub fn new(gateway_identity: ed25519::PublicKey, gateway_listeners: GatewayListeners) -> Self { GatewayConfig { gateway_identity, - gateway_owner, - gateway_listener, + gateway_listeners, } } } +#[derive(Debug, Clone)] +pub struct GatewayListeners { + pub primary: Url, + pub fallback: Option, +} + #[must_use] #[derive(Debug)] pub struct AuthenticationResponse { - pub initial_shared_key: Arc, - pub requires_key_upgrade: bool, + pub initial_shared_key: Arc, } // TODO: this should be refactored into a state machine that keeps track of its authentication state @@ -92,10 +88,10 @@ pub struct GatewayClient { authenticated: bool, bandwidth: ClientBandwidth, - gateway_address: String, + gateway_addresses: GatewayListeners, gateway_identity: ed25519::PublicKey, local_identity: Arc, - shared_key: Option>, + shared_key: Option>, connection: SocketState, packet_router: PacketRouter, bandwidth_controller: Option>, @@ -118,7 +114,7 @@ impl GatewayClient { gateway_config: GatewayConfig, local_identity: Arc, // TODO: make it mandatory. if you don't want to pass it, use `new_init` - shared_key: Option>, + shared_key: Option>, packet_router: PacketRouter, bandwidth_controller: Option>, stats_reporter: ClientStatsSender, @@ -129,7 +125,7 @@ impl GatewayClient { cfg, authenticated: false, bandwidth: ClientBandwidth::new_empty(), - gateway_address: gateway_config.gateway_listener, + gateway_addresses: gateway_config.gateway_listeners, gateway_identity: gateway_config.gateway_identity, local_identity, shared_key, @@ -148,7 +144,7 @@ impl GatewayClient { self.gateway_identity } - pub fn shared_key(&self) -> Option> { + pub fn shared_key(&self) -> Option> { self.shared_key.clone() } @@ -203,12 +199,19 @@ impl GatewayClient { #[cfg(not(target_arch = "wasm32"))] pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> { - debug!( - "Attempting to establish connection to gateway at: {}", - self.gateway_address - ); - let (ws_stream, _) = connect_async( - &self.gateway_address, + if let Some(fallback_url) = &self.gateway_addresses.fallback { + debug!( + "Attempting to establish connection to gateway at: {}, with fallback at: {fallback_url}", + self.gateway_addresses.primary + ); + } else { + debug!( + "Attempting to establish connection to gateway at: {}", + self.gateway_addresses.primary + ); + } + let (ws_stream, _) = connect_async_with_fallback( + &self.gateway_addresses, #[cfg(unix)] self.connection_fd_callback.clone(), ) @@ -221,7 +224,7 @@ impl GatewayClient { #[cfg(target_arch = "wasm32")] pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> { - let ws_stream = match JSWebsocket::new(&self.gateway_address) { + let ws_stream = match JSWebsocket::new(self.gateway_addresses.primary.as_ref()) { Ok(ws_stream) => ws_stream, Err(e) => { return Err(GatewayClientError::NetworkErrorWasm(e)); @@ -274,7 +277,7 @@ impl GatewayClient { message: ClientRequest, ) -> Result<(), GatewayClientError> { if let Some(shared_key) = self.shared_key() { - let encrypted = message.encrypt(&*shared_key)?; + let encrypted = message.encrypt(&shared_key)?; Box::pin(self.send_websocket_message_without_response(encrypted)).await?; Ok(()) } else { @@ -463,19 +466,14 @@ impl GatewayClient { async fn register( &mut self, - supported_gateway_protocol: Option, + supported_gateway_protocol: 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: {}", - !derive_aes256_gcm_siv_key - ); + log::debug!("registering with gateway"); // it's fine to instantiate it here as it's only used once (during authentication or registration) // and putting it into the GatewayClient struct would be a hassle @@ -525,75 +523,6 @@ impl GatewayClient { Ok(()) } - pub async fn upgrade_key_authenticated( - &mut self, - ) -> Result, GatewayClientError> { - info!("*** STARTING AES128CTR-HMAC KEY UPGRADE INTO AES256GCM-SIV***"); - - if !self.connection.is_established() { - return Err(GatewayClientError::ConnectionNotEstablished); - } - - if !self.authenticated { - return Err(GatewayClientError::NotAuthenticated); - } - - let Some(shared_key) = self.shared_key.as_ref() else { - return Err(GatewayClientError::NoSharedKeyAvailable); - }; - - if !shared_key.is_legacy() { - return Err(GatewayClientError::KeyAlreadyUpgraded); - } - - // make sure we have the only reference, so we could safely swap it - if Arc::strong_count(shared_key) != 1 { - return Err(GatewayClientError::KeyAlreadyInUse); - } - - assert!(shared_key.is_legacy()); - let legacy_key = shared_key.unwrap_legacy(); - let (updated_key, hkdf_salt) = legacy_key.upgrade(); - let derived_key_digest = updated_key.digest(); - - let upgrade_request = ClientRequest::UpgradeKey { - hkdf_salt, - derived_key_digest, - } - .encrypt(legacy_key)?; - - info!("sending upgrade request and awaiting the acknowledgement back"); - let (ciphertext, nonce) = match self - .send_websocket_message_with_response(upgrade_request) - .await? - { - ServerResponse::EncryptedResponse { ciphertext, nonce } => (ciphertext, nonce), - ServerResponse::Error { message } => { - return Err(GatewayClientError::GatewayError(message)) - } - other => return Err(GatewayClientError::UnexpectedResponse { name: other.name() }), - }; - - // attempt to decrypt it using NEW key - let Ok(response) = SensitiveServerResponse::decrypt(&ciphertext, &nonce, &updated_key) - else { - return Err(GatewayClientError::FatalKeyUpgradeFailure); - }; - - match response { - SensitiveServerResponse::KeyUpgradeAck { .. } => { - info!("received key upgrade acknowledgement") - } - _ => return Err(GatewayClientError::FatalKeyUpgradeFailure), - } - - // perform in memory swap and make a copy for updating storage - let zeroizing_updated_key = updated_key.zeroizing_clone(); - self.shared_key = Some(Arc::new(updated_key.into())); - - Ok(zeroizing_updated_key) - } - async fn send_authenticate_request_and_handle_response( &mut self, msg: ClientControlRequest, @@ -606,17 +535,14 @@ impl GatewayClient { upgrade_mode, } => { 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"); + error!("the gateway insists on using v{protocol_version} protocol which is not supported by this client"); return Err(GatewayClientError::AuthenticationFailure); } self.authenticated = status; self.bandwidth .update_and_maybe_log(bandwidth_remaining, upgrade_mode); - self.negotiated_protocol = protocol_version; + self.negotiated_protocol = Some(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") @@ -629,27 +555,6 @@ impl GatewayClient { } } - async fn authenticate_v1(&mut self) -> Result<(), GatewayClientError> { - debug!("using v1 authentication"); - - let Some(shared_key) = self.shared_key.as_ref() else { - return Err(GatewayClientError::NoSharedKeyAvailable); - }; - - let self_address = self - .local_identity - .public_key() - .derive_destination_address(); - - let msg = ClientControlRequest::new_legacy_authenticate( - self_address, - shared_key, - self.cfg.bandwidth.require_tickets, - )?; - self.send_authenticate_request_and_handle_response(msg) - .await - } - async fn authenticate_v2( &mut self, requested_protocol_version: GatewayProtocolVersion, @@ -670,30 +575,22 @@ impl GatewayClient { async fn authenticate( &mut self, - supported_gateway_protocol: Option, + requested_protocol_version: GatewayProtocolVersion, ) -> Result<(), GatewayClientError> { if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } debug!("authenticating with gateway"); - 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 - } + // use the highest possible protocol version the gateway has announced support for + self.authenticate_v2(requested_protocol_version).await } /// Helper method to either call register or authenticate based on self.shared_key value #[instrument(skip_all, fields( gateway = %self.gateway_identity, - gateway_address = %self.gateway_address + gateway_address = %self.gateway_addresses.primary ) )] pub async fn perform_initial_authentication( @@ -704,15 +601,9 @@ impl GatewayClient { } // 1. check gateway's protocol version - let gw_protocol = match self.get_gateway_protocol().await { - Ok(protocol) => Some(protocol), - Err(_) => { - // if we failed to send the request, it means the gateway is running the old binary, - // so it has reset our connection - we have to reconnect - self.establish_connection().await?; - None - } - }; + // if we failed to get this request resolved, it means the gateway is on an old version + // that definitely does not support auth v2 or aes256gcm, so we bail + let gw_protocol = self.get_gateway_protocol().await?; debug!("supported gateway protocol: {gw_protocol:?}"); @@ -727,6 +618,16 @@ impl GatewayClient { if !supports_auth_v2 { warn!("this gateway is on an old version that doesn't support authentication v2") } + + // Dropping v1 support + if !supports_auth_v2 || !supports_aes_gcm_siv { + // we can't continue + return Err(GatewayClientError::IncompatibleProtocol { + gateway: gw_protocol, + current: CURRENT_PROTOCOL_VERSION, + }); + } + if !supports_key_rotation_info { warn!("this gateway is on an old version that doesn't support key rotation packets") } @@ -736,7 +637,7 @@ impl GatewayClient { 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) + GatewayProtocolVersion::CURRENT } else { gw_protocol }; @@ -746,7 +647,6 @@ impl GatewayClient { return if let Some(shared_key) = &self.shared_key { Ok(AuthenticationResponse { initial_shared_key: Arc::clone(shared_key), - requires_key_upgrade: shared_key.is_legacy() && supports_aes_gcm_siv, }) } else { Err(GatewayClientError::AuthenticationFailureWithPreexistingSharedKey) @@ -761,11 +661,8 @@ impl GatewayClient { #[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; - Ok(AuthenticationResponse { initial_shared_key: Arc::clone(shared_key), - requires_key_upgrade, }) } else { Err(GatewayClientError::AuthenticationFailure) @@ -781,7 +678,6 @@ impl GatewayClient { // so no upgrades are required Ok(AuthenticationResponse { initial_shared_key: Arc::clone(shared_key), - requires_key_upgrade: false, }) } } @@ -1143,7 +1039,12 @@ impl GatewayClient { } // if we're reconnecting, because we lost connection, we need to re-authenticate the connection - self.authenticate(self.negotiated_protocol).await?; + if let Some(negotiated_protocol) = self.negotiated_protocol { + self.authenticate(negotiated_protocol).await?; + } else { + // This should never happen, because it would mean we're not registered + return Err(GatewayClientError::NotRegistered); + } // this call is NON-blocking self.start_listening_for_mixnet_messages()?; @@ -1188,7 +1089,7 @@ pub struct InitOnly; impl GatewayClient { // for initialisation we do not need credential storage. Though it's still a bit weird we have to set the generic... pub fn new_init( - gateway_listener: Url, + gateway_listeners: GatewayListeners, gateway_identity: ed25519::PublicKey, local_identity: Arc, #[cfg(unix)] connection_fd_callback: Option>, @@ -1207,7 +1108,7 @@ impl GatewayClient { cfg: GatewayClientConfig::default().with_disabled_credentials_mode(true), authenticated: false, bandwidth: ClientBandwidth::new_empty(), - gateway_address: gateway_listener.to_string(), + gateway_addresses: gateway_listeners, gateway_identity, local_identity, shared_key: None, @@ -1239,7 +1140,7 @@ impl GatewayClient { cfg: self.cfg, authenticated: self.authenticated, bandwidth: self.bandwidth, - gateway_address: self.gateway_address, + gateway_addresses: self.gateway_addresses, gateway_identity: self.gateway_identity, local_identity: self.local_identity, shared_key: self.shared_key, diff --git a/common/client-libs/gateway-client/src/client/websockets.rs b/common/client-libs/gateway-client/src/client/websockets.rs index 2336549d5e2..147f99b8a43 100644 --- a/common/client-libs/gateway-client/src/client/websockets.rs +++ b/common/client-libs/gateway-client/src/client/websockets.rs @@ -1,3 +1,5 @@ +#[cfg(not(target_arch = "wasm32"))] +use crate::client::GatewayListeners; use crate::error::GatewayClientError; use nym_http_api_client::HickoryDnsResolver; @@ -85,3 +87,35 @@ pub(crate) async fn connect_async( source: Box::new(error), }) } + +#[cfg(not(target_arch = "wasm32"))] +pub(crate) async fn connect_async_with_fallback( + endpoints: &GatewayListeners, + #[cfg(unix)] connection_fd_callback: Option>, +) -> Result<(WebSocketStream>, Response), GatewayClientError> { + match connect_async( + endpoints.primary.as_ref(), + #[cfg(unix)] + connection_fd_callback.clone(), + ) + .await + { + Ok(inner) => Ok(inner), + Err(e) => { + if let Some(fallback) = &endpoints.fallback { + tracing::warn!( + "Main endpoint failed {} : {e}, trying fallback : {fallback}", + endpoints.primary + ); + connect_async( + fallback.as_ref(), + #[cfg(unix)] + connection_fd_callback, + ) + .await + } else { + Err(e) + } + } + } +} diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index 9f97b7c7f58..e0098af50e9 100644 --- a/common/client-libs/gateway-client/src/error.rs +++ b/common/client-libs/gateway-client/src/error.rs @@ -83,6 +83,9 @@ pub enum GatewayClientError { #[error("Client is not authenticated")] NotAuthenticated, + #[error("Client is not registered")] + NotRegistered, + #[error("Client does not have enough bandwidth: estimated {0}, remaining: {1}")] NotEnoughBandwidth(i64, i64), @@ -116,8 +119,8 @@ pub enum GatewayClientError { #[error("Failed to send mixnet message")] MixnetMsgSenderFailedToSend, - #[error("Attempted to negotiate connection with gateway using incompatible protocol version. Ours is {current} and the gateway reports {gateway:?}")] - IncompatibleProtocol { gateway: Option, current: u8 }, + #[error("Attempted to negotiate connection with gateway using incompatible protocol version. Ours is {current} and the gateway reports {gateway}")] + IncompatibleProtocol { gateway: u8, current: u8 }, #[error( "The packet router hasn't been set - are you sure you started up the client correctly?" diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index 394ba352d0c..a8041f32acd 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -7,9 +7,7 @@ use tracing::{error, warn}; use tungstenite::{protocol::Message, Error as WsError}; pub use client::{config::GatewayClientConfig, GatewayClient, GatewayConfig}; -pub use nym_gateway_requests::shared_key::{ - LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey, -}; +pub use nym_gateway_requests::shared_key::SharedSymmetricKey; pub use packet_router::{ AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender, PacketRouter, @@ -47,7 +45,7 @@ pub(crate) fn cleanup_socket_messages( pub(crate) fn try_decrypt_binary_message( bin_msg: Vec, - shared_keys: &SharedGatewayKey, + shared_keys: &SharedSymmetricKey, ) -> Option> { match BinaryResponse::try_from_encrypted_tagged_bytes(bin_msg, shared_keys) { Ok(bin_response) => match bin_response { diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index af98d1b7972..1a464aa3045 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -10,7 +10,7 @@ use crate::{cleanup_socket_messages, try_decrypt_binary_message}; use futures::channel::oneshot; use futures::stream::{SplitSink, SplitStream}; use futures::{SinkExt, StreamExt}; -use nym_gateway_requests::shared_key::SharedGatewayKey; +use nym_gateway_requests::shared_key::SharedSymmetricKey; use nym_gateway_requests::{ SendResponse, SensitiveServerResponse, ServerResponse, SimpleGatewayRequestsError, }; @@ -66,7 +66,7 @@ pub(crate) struct PartiallyDelegatedHandle { struct PartiallyDelegatedRouter { packet_router: PacketRouter, - shared_key: Arc, + shared_key: Arc, client_bandwidth: ClientBandwidth, stream_return: SplitStreamSender, @@ -76,7 +76,7 @@ struct PartiallyDelegatedRouter { impl PartiallyDelegatedRouter { fn new( packet_router: PacketRouter, - shared_key: Arc, + shared_key: Arc, client_bandwidth: ClientBandwidth, stream_return: SplitStreamSender, stream_return_requester: oneshot::Receiver<()>, @@ -214,11 +214,6 @@ impl PartiallyDelegatedRouter { SensitiveServerResponse::RememberMeAck {} => { info!("received remember me acknowledgement"); } - SensitiveServerResponse::KeyUpgradeAck {} => { - warn!( - "received illegal key upgrade acknowledgement in an authenticated client" - ); - } _ => { warn!("received unknown SensitiveServerResponse"); } @@ -294,7 +289,7 @@ impl PartiallyDelegatedHandle { pub(crate) fn split_and_listen_for_mixnet_messages( conn: WsConn, packet_router: PacketRouter, - shared_key: Arc, + shared_key: Arc, client_bandwidth: ClientBandwidth, shutdown: ShutdownToken, ) -> Self { diff --git a/common/gateway-requests/src/authentication/encrypted_address.rs b/common/gateway-requests/src/authentication/encrypted_address.rs deleted file mode 100644 index 772b8ca91b2..00000000000 --- a/common/gateway-requests/src/authentication/encrypted_address.rs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2020-2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::shared_key::{SharedGatewayKey, SharedKeyUsageError}; -use nym_sphinx::DestinationAddressBytes; -use thiserror::Error; - -/// Replacement for what used to be an `AuthToken`. -/// -/// Replacement for what used to be an `AuthToken`. We used to be generating an `AuthToken` based on -/// local secret and remote address in order to allow for authentication. Due to changes in registration -/// and the fact we are deriving a shared key, we are encrypting remote's address with the previously -/// derived shared key. If the value is as expected, then authentication is successful. -#[derive(Debug, PartialEq, Eq, Hash, Clone)] -// this is no longer constant size due to the differences in ciphertext between aes128ctr and aes256gcm-siv (inclusion of tag) -pub struct EncryptedAddressBytes(Vec); - -impl From> for EncryptedAddressBytes { - fn from(encrypted_address: Vec) -> Self { - EncryptedAddressBytes(encrypted_address) - } -} - -#[derive(Debug, Error)] -pub enum EncryptedAddressConversionError { - #[error("Failed to decode the encrypted address - {0}")] - DecodeError(#[from] bs58::decode::Error), -} - -impl EncryptedAddressBytes { - pub fn new( - address: &DestinationAddressBytes, - key: &SharedGatewayKey, - nonce: &[u8], - ) -> Result { - let ciphertext = key.encrypt_naive(address.as_bytes_ref(), Some(nonce))?; - - Ok(EncryptedAddressBytes(ciphertext)) - } - - pub fn verify( - &self, - address: &DestinationAddressBytes, - key: &SharedGatewayKey, - nonce: &[u8], - ) -> bool { - let Ok(reconstructed) = Self::new(address, key, nonce) else { - return false; - }; - self == &reconstructed - } - - pub fn as_bytes(&self) -> &[u8] { - &self.0 - } - - pub fn try_from_base58_string>( - val: S, - ) -> Result { - let decoded = bs58::decode(val.into()).into_vec()?; - Ok(EncryptedAddressBytes(decoded)) - } - - pub fn to_base58_string(self) -> String { - bs58::encode(self.0).into_string() - } -} - -impl From for String { - fn from(val: EncryptedAddressBytes) -> Self { - val.to_base58_string() - } -} diff --git a/common/gateway-requests/src/authentication/mod.rs b/common/gateway-requests/src/authentication/mod.rs deleted file mode 100644 index 3e2a4c4d5a7..00000000000 --- a/common/gateway-requests/src/authentication/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2020 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -pub mod encrypted_address; diff --git a/common/gateway-requests/src/lib.rs b/common/gateway-requests/src/lib.rs index a0f44b20fc5..b63b29dca1a 100644 --- a/common/gateway-requests/src/lib.rs +++ b/common/gateway-requests/src/lib.rs @@ -7,17 +7,12 @@ use nym_sphinx::params::GatewayIntegrityHmacAlgorithm; pub use types::*; -pub mod authentication; pub mod models; pub mod registration; pub mod shared_key; pub mod types; -pub use shared_key::helpers::SymmetricKey; -pub use shared_key::legacy::{LegacySharedKeySize, LegacySharedKeys}; -pub use shared_key::{ - SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey, -}; +pub use shared_key::{SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey}; pub type GatewayProtocolVersion = u8; @@ -29,7 +24,7 @@ pub const CURRENT_PROTOCOL_VERSION: GatewayProtocolVersion = UPGRADE_MODE_VERSIO // 1 - initial release // 2 - changes to client credentials structure // 3 - change to AES-GCM-SIV and non-zero IVs -// 4 - introduction of v2 authentication protocol to prevent reply attacks +// 4 - introduction of v2 authentication protocol to prevent replay attacks // 5 - add key rotation information to the serialised mix packet // 6 - support for 'upgrade mode' pub const INITIAL_PROTOCOL_VERSION: GatewayProtocolVersion = 1; diff --git a/common/gateway-requests/src/registration/handshake/client.rs b/common/gateway-requests/src/registration/handshake/client.rs index 7f2a3ccb19e..9d2584a4a35 100644 --- a/common/gateway-requests/src/registration/handshake/client.rs +++ b/common/gateway-requests/src/registration/handshake/client.rs @@ -5,7 +5,7 @@ use crate::registration::handshake::messages::{Finalization, GatewayMaterialExch use crate::registration::handshake::state::State; use crate::registration::handshake::HandshakeResult; use crate::registration::handshake::{error::HandshakeError, WsItem}; -use crate::{GatewayProtocolVersionExt, INITIAL_PROTOCOL_VERSION}; +use crate::GatewayProtocolVersionExt; use futures::{Sink, Stream}; use rand::{CryptoRng, RngCore}; use tracing::info; @@ -18,11 +18,11 @@ impl State<'_, S, R> { R: CryptoRng + RngCore, { // 1. if we're using non-legacy, i.e. aes256gcm-siv derivation, generate initiator salt for kdf - let maybe_hkdf_salt = self.maybe_generate_initiator_salt(); + let hkdf_salt = self.generate_initiator_salt(); // 1. send ed25519 pubkey alongside ephemeral x25519 pubkey and a hkdf salt if we're using non-legacy client - // LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_SALT - let init_message = self.init_message(maybe_hkdf_salt.clone()); + // LOCAL_ID_PUBKEY || EPHEMERAL_KEY || SALT + let init_message = self.init_message(hkdf_salt.clone()); self.send_handshake_data(init_message).await?; // 2. wait for response with remote x25519 pubkey as well as encrypted signature @@ -33,23 +33,20 @@ impl State<'_, S, R> { // 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(), + version: gateway_protocol, }); } - 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() { + if 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::::(g^xy) - self.derive_shared_key(&mid_res.ephemeral_dh, maybe_hkdf_salt.as_deref()); + self.derive_shared_key(&mid_res.ephemeral_dh, &hkdf_salt); // 4. verify the received signature using the locally derived keys self.verify_remote_key_material(&mid_res.materials, &mid_res.ephemeral_dh)?; diff --git a/common/gateway-requests/src/registration/handshake/gateway.rs b/common/gateway-requests/src/registration/handshake/gateway.rs index cea8fd67de9..fab00d0dd5f 100644 --- a/common/gateway-requests/src/registration/handshake/gateway.rs +++ b/common/gateway-requests/src/registration/handshake/gateway.rs @@ -56,10 +56,7 @@ impl State<'_, S, R> { // 2. derive shared keys locally // hkdf::::(g^xy) - self.derive_shared_key( - &init_message.ephemeral_dh, - init_message.initiator_salt.as_deref(), - ); + self.derive_shared_key(&init_message.ephemeral_dh, &init_message.initiator_salt); // 3. send ephemeral x25519 pubkey alongside the encrypted signature // g^y || AES(k, sig(gate_priv, (g^y || g^x)) diff --git a/common/gateway-requests/src/registration/handshake/messages.rs b/common/gateway-requests/src/registration/handshake/messages.rs index 9958c17ba10..a693ec5f430 100644 --- a/common/gateway-requests/src/registration/handshake/messages.rs +++ b/common/gateway-requests/src/registration/handshake/messages.rs @@ -4,7 +4,7 @@ use crate::registration::handshake::error::HandshakeError; use crate::registration::handshake::KDF_SALT_LENGTH; use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_crypto::symmetric::aead::{nonce_size, tag_size}; +use nym_crypto::symmetric::aead::{nonce_size, tag_size, Nonce}; use nym_sphinx::params::GatewayEncryptionAlgorithm; // it is vital nobody changes the serialisation implementation unless you have an EXTREMELY good reason, @@ -21,13 +21,13 @@ pub trait HandshakeMessage { pub struct Initialisation { pub identity: ed25519::PublicKey, pub ephemeral_dh: x25519::PublicKey, - pub initiator_salt: Option>, + pub initiator_salt: Vec, } #[derive(Debug)] pub struct MaterialExchange { pub signature_ciphertext: Vec, - pub nonce: Option>, + pub nonce: Nonce, } impl MaterialExchange { @@ -61,21 +61,16 @@ impl Finalization { } impl HandshakeMessage for Initialisation { - // LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_SALT + // LOCAL_ID_PUBKEY || EPHEMERAL_KEY || SALT // Eventually the ID_PUBKEY prefix will get removed and recipient will know // initializer's identity from another source. fn into_bytes(self) -> Vec { - let bytes = self - .identity + self.identity .to_bytes() .into_iter() - .chain(self.ephemeral_dh.to_bytes()); - - if let Some(salt) = self.initiator_salt { - bytes.chain(salt).collect() - } else { - bytes.collect() - } + .chain(self.ephemeral_dh.to_bytes()) + .chain(self.initiator_salt) + .collect() } // this will need to be adjusted when REMOTE_ID_PUBKEY is removed @@ -83,9 +78,8 @@ impl HandshakeMessage for Initialisation { where Self: Sized, { - let legacy_len = ed25519::PUBLIC_KEY_LENGTH + x25519::PUBLIC_KEY_SIZE; - let current_len = legacy_len + KDF_SALT_LENGTH; - if bytes.len() != legacy_len && bytes.len() != current_len { + let current_len = ed25519::PUBLIC_KEY_LENGTH + x25519::PUBLIC_KEY_SIZE + KDF_SALT_LENGTH; + if bytes.len() != current_len { return Err(HandshakeError::MalformedRequest); } @@ -95,14 +89,13 @@ impl HandshakeMessage for Initialisation { // 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(); + let ephemeral_dh = x25519::PublicKey::from_bytes( + &bytes + [ed25519::PUBLIC_KEY_LENGTH..ed25519::PUBLIC_KEY_LENGTH + x25519::PUBLIC_KEY_SIZE], + ) + .unwrap(); - let initiator_salt = if bytes.len() == legacy_len { - None - } else { - Some(bytes[legacy_len..].to_vec()) - }; + let initiator_salt = bytes[ed25519::PUBLIC_KEY_LENGTH + x25519::PUBLIC_KEY_SIZE..].to_vec(); Ok(Initialisation { identity, @@ -115,43 +108,31 @@ impl HandshakeMessage for Initialisation { impl HandshakeMessage for MaterialExchange { // AES(k, SIG(PRIV_GATE, G^y || G^x)) fn into_bytes(self) -> Vec { - if let Some(nonce) = self.nonce { - self.signature_ciphertext - .iter() - .cloned() - .chain(nonce) - .collect() - } else { - self.signature_ciphertext.to_vec() - } + self.signature_ciphertext + .iter() + .cloned() + .chain(self.nonce) + .collect() } fn try_from_bytes(bytes: &[u8]) -> Result where Self: Sized, { - // we expect to receive either: - // LEGACY: ed25519 signature ciphertext (64 bytes) // CURRENT: ed25519 signature ciphertext (+ tag) + AES256-GCM-SIV nonce (76 bytes) - let legacy_len = ed25519::SIGNATURE_LENGTH; - let current_len = legacy_len + let current_len = ed25519::SIGNATURE_LENGTH + tag_size::() + nonce_size::(); - if bytes.len() != legacy_len && bytes.len() != current_len { + if bytes.len() != current_len { return Err(HandshakeError::MalformedResponse); } - let (signature_ciphertext, nonce) = if bytes.len() == current_len { - let ciphertext_len = - ed25519::SIGNATURE_LENGTH + tag_size::(); - ( - bytes[..ciphertext_len].to_vec(), - Some(bytes[ciphertext_len..].to_vec()), - ) - } else { - (bytes.to_vec(), None) - }; + let ciphertext_len = ed25519::SIGNATURE_LENGTH + tag_size::(); + let signature_ciphertext = bytes[..ciphertext_len].to_vec(); + + // SAFETY: we know the bytes have correct length + let nonce = Nonce::::clone_from_slice(&bytes[ciphertext_len..]); Ok(MaterialExchange { signature_ciphertext, diff --git a/common/gateway-requests/src/registration/handshake/mod.rs b/common/gateway-requests/src/registration/handshake/mod.rs index 8b1972f921c..40b5729bb80 100644 --- a/common/gateway-requests/src/registration/handshake/mod.rs +++ b/common/gateway-requests/src/registration/handshake/mod.rs @@ -3,7 +3,7 @@ use self::error::HandshakeError; use crate::registration::handshake::state::State; -use crate::{GatewayProtocolVersion, SharedGatewayKey}; +use crate::{GatewayProtocolVersion, SharedSymmetricKey}; use futures::future::BoxFuture; use futures::{Sink, Stream}; use nym_crypto::asymmetric::ed25519; @@ -48,7 +48,7 @@ impl Future for GatewayHandshake<'_> { #[derive(Debug, PartialEq)] pub struct HandshakeResult { pub negotiated_protocol: GatewayProtocolVersion, - pub derived_key: SharedGatewayKey, + pub derived_key: SharedSymmetricKey, } pub fn client_handshake<'a, S, R>( @@ -56,7 +56,7 @@ pub fn client_handshake<'a, S, R>( ws_stream: &'a mut S, identity: &'a ed25519::KeyPair, gateway_pubkey: ed25519::PublicKey, - gateway_protocol: Option, + gateway_protocol: GatewayProtocolVersion, #[cfg(not(target_arch = "wasm32"))] shutdown_token: ShutdownToken, ) -> GatewayHandshake<'a> where @@ -84,7 +84,7 @@ pub fn gateway_handshake<'a, S, R>( ws_stream: &'a mut S, identity: &'a ed25519::KeyPair, received_init_payload: Vec, - requested_client_protocol: Option, + requested_client_protocol: GatewayProtocolVersion, shutdown_token: ShutdownToken, ) -> GatewayHandshake<'a> where @@ -125,7 +125,7 @@ DONE(status) #[cfg(test)] mod tests { use super::*; - use crate::{ClientControlRequest, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION}; + use crate::{ClientControlRequest, CURRENT_PROTOCOL_VERSION}; use anyhow::{bail, Context}; use futures::StreamExt; use nym_test_utils::helpers::u64_seeded_rng; @@ -221,7 +221,7 @@ mod tests { client.socket, client.keys, *gateway.keys.public_key(), - Some(CURRENT_PROTOCOL_VERSION), + CURRENT_PROTOCOL_VERSION, ShutdownToken::default(), ); @@ -235,7 +235,7 @@ mod tests { gateway.socket, gateway.keys, init_msg, - Some(CURRENT_PROTOCOL_VERSION), + CURRENT_PROTOCOL_VERSION, ShutdownToken::default(), ); @@ -261,7 +261,7 @@ mod tests { client.socket, client.keys, *gateway.keys.public_key(), - Some(CURRENT_PROTOCOL_VERSION + 42), + CURRENT_PROTOCOL_VERSION + 42, ShutdownToken::default(), ); @@ -274,7 +274,7 @@ mod tests { gateway.socket, gateway.keys, init_msg, - Some(CURRENT_PROTOCOL_VERSION + 42), + CURRENT_PROTOCOL_VERSION + 42, ShutdownToken::default(), ); @@ -292,46 +292,4 @@ mod tests { 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(()) - } } diff --git a/common/gateway-requests/src/registration/handshake/state.rs b/common/gateway-requests/src/registration/handshake/state.rs index 1e51fda37b1..540773243ad 100644 --- a/common/gateway-requests/src/registration/handshake/state.rs +++ b/common/gateway-requests/src/registration/handshake/state.rs @@ -5,12 +5,11 @@ use crate::registration::handshake::error::HandshakeError; use crate::registration::handshake::messages::{ HandshakeMessage, Initialisation, MaterialExchange, }; -use crate::registration::handshake::{HandshakeResult, SharedGatewayKey, WsItem, KDF_SALT_LENGTH}; -use crate::shared_key::SharedKeySize; -use crate::{ - types, GatewayProtocolVersion, GatewayProtocolVersionExt, LegacySharedKeySize, - LegacySharedKeys, SharedSymmetricKey, INITIAL_PROTOCOL_VERSION, +use crate::registration::handshake::{ + HandshakeResult, SharedSymmetricKey, WsItem, KDF_SALT_LENGTH, }; +use crate::shared_key::SharedKeySize; +use crate::{types, GatewayProtocolVersion}; use futures::{Sink, SinkExt, Stream, StreamExt}; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_crypto::symmetric::aead::random_nonce; @@ -48,17 +47,15 @@ pub(crate) struct State<'a, S, R> { ephemeral_keypair: x25519::KeyPair, /// The derived shared key using the ephemeral keys of both parties. - derived_shared_keys: Option, + derived_shared_keys: Option, /// The known or received public identity key of the remote. /// Ideally it would always be known before the handshake was initiated. remote_pubkey: Option, /// 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, + /// additional features + protocol_version: GatewayProtocolVersion, // channel to receive shutdown signal #[cfg(not(target_arch = "wasm32"))] @@ -71,7 +68,7 @@ impl<'a, S, R> State<'a, S, R> { ws_stream: &'a mut S, identity: &'a ed25519::KeyPair, remote_pubkey: Option, - protocol_version: Option, + protocol_version: GatewayProtocolVersion, #[cfg(not(target_arch = "wasm32"))] shutdown_token: ShutdownToken, ) -> Self where @@ -96,31 +93,27 @@ impl<'a, S, R> State<'a, S, R> { self.ephemeral_keypair.public_key() } - pub(crate) fn proposed_protocol_version(&self) -> Option { + pub(crate) fn proposed_protocol_version(&self) -> GatewayProtocolVersion { self.protocol_version } pub(crate) fn set_protocol_version(&mut self, protocol_version: GatewayProtocolVersion) { - self.protocol_version = Some(protocol_version); + self.protocol_version = protocol_version; } - pub(crate) fn maybe_generate_initiator_salt(&mut self) -> Option> + pub(crate) fn generate_initiator_salt(&mut self) -> Vec where R: CryptoRng + RngCore, { - if self.protocol_version.supports_aes256_gcm_siv() { - let mut salt = vec![0u8; KDF_SALT_LENGTH]; - self.rng.fill_bytes(&mut salt); - Some(salt) - } else { - None - } + let mut salt = vec![0u8; KDF_SALT_LENGTH]; + self.rng.fill_bytes(&mut salt); + salt } - // LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_SALT + // LOCAL_ID_PUBKEY || EPHEMERAL_KEY || SALT // Eventually the ID_PUBKEY prefix will get removed and recipient will know // initializer's identity from another source. - pub(crate) fn init_message(&self, initiator_salt: Option>) -> Initialisation { + pub(crate) fn init_message(&self, initiator_salt: Vec) -> Initialisation { Initialisation { identity: *self.identity.public_key(), ephemeral_dh: *self.ephemeral_keypair.public_key(), @@ -138,23 +131,19 @@ impl<'a, S, R> State<'a, S, R> { pub(crate) fn derive_shared_key( &mut self, remote_ephemeral_key: &x25519::PublicKey, - initiator_salt: Option<&[u8]>, + initiator_salt: &[u8], ) { let dh_result = self .ephemeral_keypair .private_key() .diffie_hellman(remote_ephemeral_key); - let key_size = if self.protocol_version.supports_aes256_gcm_siv() { - SharedKeySize::to_usize() - } else { - LegacySharedKeySize::to_usize() - }; + let key_size = SharedKeySize::to_usize(); // 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::( - initiator_salt, + Some(initiator_salt), &dh_result, None, key_size, @@ -162,17 +151,10 @@ impl<'a, S, R> State<'a, S, R> { .expect("somehow too long okm was provided"); // 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) - }; + #[allow(clippy::expect_used)] + let shared_key = SharedSymmetricKey::try_from_bytes(&okm) + .expect("okm was expanded to incorrect length!"); + self.derived_shared_keys = Some(shared_key) } @@ -191,12 +173,8 @@ impl<'a, S, R> State<'a, S, R> { .collect(); let signature = self.identity.private_key().sign(plaintext); - let nonce = if self.protocol_version.supports_aes256_gcm_siv() { - let mut rng = thread_rng(); - Some(random_nonce::(&mut rng).to_vec()) - } else { - None - }; + let mut rng = thread_rng(); + let nonce = random_nonce::(&mut rng); // SAFETY: this function is only called after the local key has already been derived #[allow(clippy::expect_used)] @@ -204,7 +182,7 @@ impl<'a, S, R> State<'a, S, R> { .derived_shared_keys .as_ref() .expect("shared key was not derived!") - .encrypt_naive(&signature.to_bytes(), nonce.as_deref())?; + .encrypt(&signature.to_bytes(), &nonce)?; Ok(MaterialExchange { signature_ciphertext, @@ -224,15 +202,10 @@ impl<'a, S, R> State<'a, S, R> { .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.protocol_version.supports_aes256_gcm_siv() && remote_response.nonce.is_none() { - return Err(HandshakeError::MissingNonceForCurrentKey); - } - // first decrypt received data - let decrypted_signature = derived_shared_key.decrypt_naive( + let decrypted_signature = derived_shared_key.decrypt( &remote_response.signature_ciphertext, - remote_response.nonce.as_deref(), + &remote_response.nonce, )?; // now verify signature itself @@ -262,7 +235,7 @@ impl<'a, S, R> State<'a, S, R> { #[allow(clippy::complexity)] fn on_wg_msg( msg: Option, - ) -> Result, Option)>, HandshakeError> { + ) -> Result, GatewayProtocolVersion)>, HandshakeError> { let Some(msg) = msg else { return Err(HandshakeError::ClosedStream); }; @@ -303,7 +276,7 @@ impl<'a, S, R> State<'a, S, R> { #[cfg(not(target_arch = "wasm32"))] async fn _receive_handshake_message_bytes( &mut self, - ) -> Result<(Vec, Option), HandshakeError> + ) -> Result<(Vec, GatewayProtocolVersion), HandshakeError> where S: Stream + Unpin, { @@ -324,7 +297,7 @@ impl<'a, S, R> State<'a, S, R> { #[cfg(target_arch = "wasm32")] async fn _receive_handshake_message_bytes( &mut self, - ) -> Result<(Vec, Option), HandshakeError> + ) -> Result<(Vec, GatewayProtocolVersion), HandshakeError> where S: Stream + Unpin, { @@ -339,7 +312,7 @@ impl<'a, S, R> State<'a, S, R> { pub(crate) async fn receive_handshake_message( &mut self, - ) -> Result<(M, Option), HandshakeError> + ) -> Result<(M, GatewayProtocolVersion), HandshakeError> where S: Stream + Unpin, M: HandshakeMessage, @@ -396,9 +369,7 @@ impl<'a, S, R> State<'a, S, R> { // 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), + negotiated_protocol: self.proposed_protocol_version(), derived_key: self.derived_shared_keys.unwrap(), } } diff --git a/common/gateway-requests/src/shared_key.rs b/common/gateway-requests/src/shared_key.rs new file mode 100644 index 00000000000..912b802cf41 --- /dev/null +++ b/common/gateway-requests/src/shared_key.rs @@ -0,0 +1,141 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_crypto::blake3; +use nym_crypto::crypto_hash::compute_digest; +use nym_crypto::generic_array::{typenum::Unsigned, GenericArray}; +use nym_crypto::symmetric::aead::{ + self, nonce_size, random_nonce, AeadError, AeadKey, KeySizeUser, Nonce, +}; +use nym_pemstore::traits::PemStorableKey; +use nym_sphinx::params::GatewayEncryptionAlgorithm; +use rand::thread_rng; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; + +pub type SharedKeySize = ::KeySize; + +#[derive(Debug, Error)] +pub enum SharedKeyUsageError { + #[error("the request is too short")] + TooShortRequest, + + #[error("provided MAC is invalid")] + InvalidMac, + + #[error("the provided nonce (or legacy IV) did not have the expected length")] + MalformedNonce, + + #[error("did not provide a valid nonce for aead encryption")] + MissingAeadNonce, + + #[error("failed to either encrypt or decrypt provided message")] + AeadFailure(#[from] AeadError), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)] +pub struct SharedSymmetricKey(AeadKey); + +type KeySize = ::KeySize; + +#[derive(Debug, Clone, Copy, Error)] +pub enum SharedKeyConversionError { + #[error("the string representation of the shared key was malformed: {0}")] + DecodeError(#[from] bs58::decode::Error), + #[error( + "the received shared keys had invalid size. Got: {received}, but expected: {expected}" + )] + InvalidSharedKeysSize { received: usize, expected: usize }, +} + +impl SharedSymmetricKey { + pub fn random_nonce(&self) -> Nonce { + let mut rng = thread_rng(); + random_nonce::(&mut rng) + } + + pub fn nonce_size(&self) -> usize { + nonce_size::() + } + + pub fn validate_aead_nonce( + raw: &[u8], + ) -> Result, SharedKeyUsageError> { + if raw.len() != nonce_size::() { + return Err(SharedKeyUsageError::MalformedNonce); + } + Ok(Nonce::::clone_from_slice(raw)) + } + pub fn try_from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != KeySize::to_usize() { + return Err(SharedKeyConversionError::InvalidSharedKeysSize { + received: bytes.len(), + expected: KeySize::to_usize(), + }); + } + + Ok(SharedSymmetricKey(GenericArray::clone_from_slice(bytes))) + } + + pub fn zeroizing_clone(&self) -> Zeroizing { + Zeroizing::new(SharedSymmetricKey(self.0)) + } + + pub fn digest(&self) -> Vec { + compute_digest::(self.as_bytes()).to_vec() + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_slice() + } + + pub fn to_bytes(&self) -> Vec { + self.0.iter().copied().collect() + } + + pub fn try_from_base58_string>( + val: S, + ) -> Result { + let bs58_str = Zeroizing::new(val.into()); + let decoded = Zeroizing::new(bs58::decode(bs58_str).into_vec()?); + Self::try_from_bytes(&decoded) + } + + pub fn to_base58_string(&self) -> String { + let bytes = Zeroizing::new(self.to_bytes()); + bs58::encode(bytes).into_string() + } + + pub fn encrypt( + &self, + plaintext: &[u8], + nonce: &Nonce, + ) -> Result, SharedKeyUsageError> { + aead::encrypt::(&self.0, nonce, plaintext).map_err(Into::into) + } + + pub fn decrypt( + &self, + ciphertext: &[u8], + nonce: &Nonce, + ) -> Result, SharedKeyUsageError> { + aead::decrypt::(&self.0, nonce, ciphertext).map_err(Into::into) + } +} + +impl PemStorableKey for SharedSymmetricKey { + type Error = SharedKeyConversionError; + + fn pem_type() -> &'static str { + "AES-256-GCM-SIV GATEWAY SHARED KEY" + } + + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + + fn from_bytes(bytes: &[u8]) -> Result { + Self::try_from_bytes(bytes) + } +} diff --git a/common/gateway-requests/src/shared_key/helpers.rs b/common/gateway-requests/src/shared_key/helpers.rs deleted file mode 100644 index ef034f9752a..00000000000 --- a/common/gateway-requests/src/shared_key/helpers.rs +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::{LegacySharedKeys, SharedGatewayKey, SharedKeyUsageError, SharedSymmetricKey}; -use nym_crypto::symmetric::aead::random_nonce; -use nym_crypto::symmetric::stream_cipher::random_iv; -use nym_sphinx::params::{GatewayEncryptionAlgorithm, LegacyGatewayEncryptionAlgorithm}; -use rand::thread_rng; - -pub trait SymmetricKey { - fn random_nonce_or_iv(&self) -> Vec; - - fn encrypt( - &self, - plaintext: &[u8], - nonce: Option<&[u8]>, - ) -> Result, SharedKeyUsageError>; - - fn decrypt( - &self, - ciphertext: &[u8], - nonce: Option<&[u8]>, - ) -> Result, SharedKeyUsageError>; -} - -impl SymmetricKey for SharedGatewayKey { - fn random_nonce_or_iv(&self) -> Vec { - self.random_nonce_or_iv() - } - - fn encrypt( - &self, - plaintext: &[u8], - nonce: Option<&[u8]>, - ) -> Result, SharedKeyUsageError> { - self.encrypt(plaintext, nonce) - } - - fn decrypt( - &self, - ciphertext: &[u8], - nonce: Option<&[u8]>, - ) -> Result, SharedKeyUsageError> { - self.decrypt(ciphertext, nonce) - } -} - -impl SymmetricKey for SharedSymmetricKey { - fn random_nonce_or_iv(&self) -> Vec { - let mut rng = thread_rng(); - - random_nonce::(&mut rng).to_vec() - } - - fn encrypt( - &self, - plaintext: &[u8], - nonce: Option<&[u8]>, - ) -> Result, SharedKeyUsageError> { - let nonce = SharedGatewayKey::validate_aead_nonce(nonce)?; - self.encrypt(plaintext, &nonce) - } - - fn decrypt( - &self, - ciphertext: &[u8], - nonce: Option<&[u8]>, - ) -> Result, SharedKeyUsageError> { - let nonce = SharedGatewayKey::validate_aead_nonce(nonce)?; - self.decrypt(ciphertext, &nonce) - } -} - -impl SymmetricKey for LegacySharedKeys { - fn random_nonce_or_iv(&self) -> Vec { - let mut rng = thread_rng(); - - random_iv::(&mut rng).to_vec() - } - - fn encrypt( - &self, - plaintext: &[u8], - nonce: Option<&[u8]>, - ) -> Result, SharedKeyUsageError> { - let iv = SharedGatewayKey::validate_cipher_iv(nonce)?; - Ok(self.encrypt_and_tag(plaintext, iv)) - } - - fn decrypt( - &self, - ciphertext: &[u8], - nonce: Option<&[u8]>, - ) -> Result, SharedKeyUsageError> { - let iv = SharedGatewayKey::validate_cipher_iv(nonce)?; - self.decrypt_tagged(ciphertext, iv) - } -} diff --git a/common/gateway-requests/src/shared_key/legacy.rs b/common/gateway-requests/src/shared_key/legacy.rs deleted file mode 100644 index 6f40fcd2ffa..00000000000 --- a/common/gateway-requests/src/shared_key/legacy.rs +++ /dev/null @@ -1,246 +0,0 @@ -// Copyright 2020-2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::registration::handshake::KDF_SALT_LENGTH; -use crate::shared_key::SharedSymmetricKey; -use crate::shared_key::{SharedKeyConversionError, SharedKeySize, SharedKeyUsageError}; -use crate::LegacyGatewayMacSize; -use nym_crypto::generic_array::{ - typenum::{Sum, Unsigned, U16}, - GenericArray, -}; -use nym_crypto::hkdf; -use nym_crypto::hmac::{compute_keyed_hmac, recompute_keyed_hmac_and_verify_tag}; -use nym_crypto::symmetric::stream_cipher::{self, CipherKey, KeySizeUser, IV}; -use nym_pemstore::traits::PemStorableKey; -use nym_sphinx::params::{ - GatewayIntegrityHmacAlgorithm, GatewaySharedKeyHkdfAlgorithm, LegacyGatewayEncryptionAlgorithm, -}; -use rand::{thread_rng, RngCore}; -use serde::{Deserialize, Serialize}; -use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; - -// shared key is as long as the encryption key and the MAC key combined. -pub type LegacySharedKeySize = Sum; - -// we're using 16 byte long key in sphinx, so let's use the same one here -type MacKeySize = U16; -type EncryptionKeySize = ::KeySize; - -/// Shared key used when computing MAC for messages exchanged between client and its gateway. -pub type MacKey = GenericArray; - -#[derive(Debug, PartialEq, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)] -pub struct LegacySharedKeys { - encryption_key: CipherKey, - mac_key: MacKey, -} - -impl LegacySharedKeys { - pub fn upgrade(&self) -> (SharedSymmetricKey, Vec) { - let mut rng = thread_rng(); - let mut salt = vec![0u8; KDF_SALT_LENGTH]; - rng.fill_bytes(&mut salt); - - let legacy_bytes = Zeroizing::new(self.to_bytes()); - #[allow(clippy::expect_used)] - let okm = hkdf::extract_then_expand::( - Some(&salt), - &legacy_bytes, - None, - 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!"); - (key, salt) - } - - pub fn upgrade_verify( - &self, - salt: &[u8], - expected_digest: &[u8], - ) -> Option { - let legacy_bytes = Zeroizing::new(self.to_bytes()); - #[allow(clippy::expect_used)] - let okm = hkdf::extract_then_expand::( - Some(salt), - &legacy_bytes, - None, - 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 { - // no need to zeroize that key since it's malformed and we won't be using it anyway - None - } else { - Some(key) - } - } - - pub fn try_from_bytes(bytes: &[u8]) -> Result { - if bytes.len() != LegacySharedKeySize::to_usize() { - return Err(SharedKeyConversionError::InvalidSharedKeysSize { - received: bytes.len(), - expected: LegacySharedKeySize::to_usize(), - }); - } - - let encryption_key = - GenericArray::clone_from_slice(&bytes[..EncryptionKeySize::to_usize()]); - let mac_key = GenericArray::clone_from_slice(&bytes[EncryptionKeySize::to_usize()..]); - - Ok(LegacySharedKeys { - encryption_key, - mac_key, - }) - } - - /// Encrypts the provided data using the optionally provided initialisation vector, - /// or a 0 value if nothing was given. - /// It does **NOT** attach any integrity macs on the produced ciphertext - pub fn encrypt_without_tagging( - &self, - data: &[u8], - iv: Option<&IV>, - ) -> Vec { - match iv { - Some(iv) => stream_cipher::encrypt::( - self.encryption_key(), - iv, - data, - ), - None => { - let zero_iv = stream_cipher::zero_iv::(); - stream_cipher::encrypt::( - self.encryption_key(), - &zero_iv, - data, - ) - } - } - } - - /// Encrypts the provided data using the optionally provided initialisation vector, - /// or a 0 value if nothing was given. Then it computes an integrity mac and concatenates it - /// with the previously produced ciphertext. - pub fn encrypt_and_tag( - &self, - data: &[u8], - iv: Option<&IV>, - ) -> Vec { - let ciphertext = self.encrypt_without_tagging(data, iv); - let mac = compute_keyed_hmac::( - self.mac_key().as_slice(), - &ciphertext, - ); - - mac.into_bytes().into_iter().chain(ciphertext).collect() - } - - pub fn decrypt_without_tag( - &self, - ciphertext: &[u8], - iv: Option<&IV>, - ) -> Result, SharedKeyUsageError> { - let zero_iv = stream_cipher::zero_iv::(); - let iv = iv.unwrap_or(&zero_iv); - Ok(stream_cipher::decrypt::( - self.encryption_key(), - iv, - ciphertext, - )) - } - - pub fn decrypt_tagged( - &self, - enc_data: &[u8], - iv: Option<&IV>, - ) -> Result, SharedKeyUsageError> { - let mac_size = LegacyGatewayMacSize::to_usize(); - if enc_data.len() < mac_size { - return Err(SharedKeyUsageError::TooShortRequest); - } - - let mac_tag = &enc_data[..mac_size]; - let message_bytes = &enc_data[mac_size..]; - - if !recompute_keyed_hmac_and_verify_tag::( - self.mac_key().as_slice(), - message_bytes, - mac_tag, - ) { - return Err(SharedKeyUsageError::InvalidMac); - } - - // couldn't have made the first borrow mutable as you can't have an immutable borrow - // together with a mutable one - let mut message_bytes_mut = message_bytes.to_vec(); - - let zero_iv = stream_cipher::zero_iv::(); - let iv = iv.unwrap_or(&zero_iv); - stream_cipher::decrypt_in_place::( - self.encryption_key(), - iv, - &mut message_bytes_mut, - ); - Ok(message_bytes_mut) - } - - pub fn encryption_key(&self) -> &CipherKey { - &self.encryption_key - } - - pub fn mac_key(&self) -> &MacKey { - &self.mac_key - } - - pub fn to_bytes(&self) -> Vec { - self.encryption_key - .iter() - .copied() - .chain(self.mac_key.iter().copied()) - .collect() - } - - pub fn try_from_base58_string>( - val: S, - ) -> Result { - let decoded = bs58::decode(val.into()).into_vec()?; - LegacySharedKeys::try_from_bytes(&decoded) - } - - pub fn to_base58_string(&self) -> String { - bs58::encode(self.to_bytes()).into_string() - } -} - -impl From for String { - fn from(keys: LegacySharedKeys) -> Self { - keys.to_base58_string() - } -} - -impl PemStorableKey for LegacySharedKeys { - type Error = SharedKeyConversionError; - - fn pem_type() -> &'static str { - // TODO: If common\nymsphinx\params\src\lib::GatewayIntegrityHmacAlgorithm changes - // the pem type needs updating! - "AES-128-CTR + HMAC-BLAKE3 GATEWAY SHARED KEYS" - } - - fn to_bytes(&self) -> Vec { - self.to_bytes() - } - - fn from_bytes(bytes: &[u8]) -> Result { - Self::try_from_bytes(bytes) - } -} diff --git a/common/gateway-requests/src/shared_key/mod.rs b/common/gateway-requests/src/shared_key/mod.rs deleted file mode 100644 index c0a72135a3f..00000000000 --- a/common/gateway-requests/src/shared_key/mod.rs +++ /dev/null @@ -1,306 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use nym_crypto::blake3; -use nym_crypto::crypto_hash::compute_digest; -use nym_crypto::generic_array::{typenum::Unsigned, GenericArray}; -use nym_crypto::symmetric::aead::{ - self, nonce_size, random_nonce, AeadError, AeadKey, KeySizeUser, Nonce, -}; -use nym_crypto::symmetric::stream_cipher::{iv_size, random_iv, IV}; -use nym_pemstore::traits::PemStorableKey; -use nym_sphinx::params::{GatewayEncryptionAlgorithm, LegacyGatewayEncryptionAlgorithm}; -use rand::thread_rng; -use serde::{Deserialize, Serialize}; -use thiserror::Error; -use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; - -pub use legacy::LegacySharedKeys; - -pub mod helpers; -pub mod legacy; - -pub type SharedKeySize = ::KeySize; - -#[derive(Debug, PartialEq, Zeroize, ZeroizeOnDrop)] -pub enum SharedGatewayKey { - Current(SharedSymmetricKey), - Legacy(LegacySharedKeys), -} - -impl SharedGatewayKey { - pub fn is_legacy(&self) -> bool { - matches!(self, SharedGatewayKey::Legacy(..)) - } - - pub fn aes128_ctr_hmac_bs58(&self) -> Option> { - match self { - SharedGatewayKey::Current(_) => None, - SharedGatewayKey::Legacy(key) => Some(Zeroizing::new(key.to_base58_string())), - } - } - - pub fn aes256_gcm_siv(&self) -> Option>> { - match self { - SharedGatewayKey::Current(key) => Some(Zeroizing::new(key.to_bytes())), - SharedGatewayKey::Legacy(_) => None, - } - } - - // 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"), - SharedGatewayKey::Legacy(key) => key, - } - } - - pub fn random_nonce_or_iv(&self) -> Vec { - let mut rng = thread_rng(); - - if self.is_legacy() { - random_iv::(&mut rng).to_vec() - } else { - random_nonce::(&mut rng).to_vec() - } - } - - pub fn random_nonce_or_zero_iv(&self) -> Option> { - if self.is_legacy() { - None - } else { - let mut rng = thread_rng(); - Some(random_nonce::(&mut rng).to_vec()) - } - } - - pub fn nonce_size(&self) -> usize { - match self { - SharedGatewayKey::Current(_) => nonce_size::(), - SharedGatewayKey::Legacy(_) => iv_size::(), - } - } -} - -impl From for SharedGatewayKey { - fn from(keys: LegacySharedKeys) -> Self { - SharedGatewayKey::Legacy(keys) - } -} - -impl From for SharedGatewayKey { - fn from(keys: SharedSymmetricKey) -> Self { - SharedGatewayKey::Current(keys) - } -} - -#[derive(Debug, Error)] -pub enum SharedKeyUsageError { - #[error("the request is too short")] - TooShortRequest, - - #[error("provided MAC is invalid")] - InvalidMac, - - #[error("the provided nonce (or legacy IV) did not have the expected length")] - MalformedNonce, - - #[error("did not provide a valid nonce for aead encryption")] - MissingAeadNonce, - - #[error("failed to either encrypt or decrypt provided message")] - AeadFailure(#[from] AeadError), -} - -impl SharedGatewayKey { - fn validate_aead_nonce( - raw: Option<&[u8]>, - ) -> Result, SharedKeyUsageError> { - let Some(raw) = raw else { - return Err(SharedKeyUsageError::MissingAeadNonce); - }; - if raw.len() != nonce_size::() { - return Err(SharedKeyUsageError::MalformedNonce); - } - Ok(Nonce::::clone_from_slice(raw)) - } - - fn validate_cipher_iv( - raw: Option<&[u8]>, - ) -> Result>, SharedKeyUsageError> { - let Some(raw) = raw else { return Ok(None) }; - let iv = if raw.is_empty() { - None - } else { - if raw.len() != iv_size::() { - return Err(SharedKeyUsageError::MalformedNonce); - } - Some(IV::::from_slice(raw)) - }; - Ok(iv) - } - - pub fn encrypt( - &self, - plaintext: &[u8], - // the best common denominator for converting into 'IV' and 'Nonce' types - raw_nonce: Option<&[u8]>, - ) -> Result, SharedKeyUsageError> { - match self { - SharedGatewayKey::Current(aes_gcm_siv) => { - let nonce = Self::validate_aead_nonce(raw_nonce)?; - aes_gcm_siv.encrypt(plaintext, &nonce) - } - SharedGatewayKey::Legacy(aes_ctr) => { - let iv = Self::validate_cipher_iv(raw_nonce)?; - Ok(aes_ctr.encrypt_and_tag(plaintext, iv)) - } - } - } - - pub fn decrypt( - &self, - ciphertext: &[u8], - // the best common denominator for converting into 'IV' and 'Nonce' types - raw_nonce: Option<&[u8]>, - ) -> Result, SharedKeyUsageError> { - match self { - SharedGatewayKey::Current(aes_gcm_siv) => { - let nonce = Self::validate_aead_nonce(raw_nonce)?; - aes_gcm_siv.decrypt(ciphertext, &nonce) - } - SharedGatewayKey::Legacy(aes_ctr) => { - let iv = Self::validate_cipher_iv(raw_nonce)?; - aes_ctr.decrypt_tagged(ciphertext, iv) - } - } - } - - // for the legacy keys do not use integrity MAC - pub fn encrypt_naive( - &self, - plaintext: &[u8], - // the best common denominator for converting into 'IV' and 'Nonce' types - raw_nonce: Option<&[u8]>, - ) -> Result, SharedKeyUsageError> { - match self { - SharedGatewayKey::Current(aes_gcm_siv) => { - let nonce = Self::validate_aead_nonce(raw_nonce)?; - aes_gcm_siv.encrypt(plaintext, &nonce) - } - SharedGatewayKey::Legacy(aes_ctr) => { - let iv = Self::validate_cipher_iv(raw_nonce)?; - Ok(aes_ctr.encrypt_without_tagging(plaintext, iv)) - } - } - } - - // for the legacy keys do not use integrity MAC - pub fn decrypt_naive( - &self, - ciphertext: &[u8], - // the best common denominator for converting into 'IV' and 'Nonce' types - raw_nonce: Option<&[u8]>, - ) -> Result, SharedKeyUsageError> { - match self { - SharedGatewayKey::Current(aes_gcm_siv) => { - let nonce = Self::validate_aead_nonce(raw_nonce)?; - aes_gcm_siv.decrypt(ciphertext, &nonce) - } - SharedGatewayKey::Legacy(aes_ctr) => { - let iv = Self::validate_cipher_iv(raw_nonce)?; - aes_ctr.decrypt_without_tag(ciphertext, iv) - } - } - } -} - -#[derive(Debug, PartialEq, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)] -pub struct SharedSymmetricKey(AeadKey); - -type KeySize = ::KeySize; - -#[derive(Debug, Clone, Copy, Error)] -pub enum SharedKeyConversionError { - #[error("the string representation of the shared key was malformed: {0}")] - DecodeError(#[from] bs58::decode::Error), - #[error( - "the received shared keys had invalid size. Got: {received}, but expected: {expected}" - )] - InvalidSharedKeysSize { received: usize, expected: usize }, -} - -impl SharedSymmetricKey { - pub fn try_from_bytes(bytes: &[u8]) -> Result { - if bytes.len() != KeySize::to_usize() { - return Err(SharedKeyConversionError::InvalidSharedKeysSize { - received: bytes.len(), - expected: KeySize::to_usize(), - }); - } - - Ok(SharedSymmetricKey(GenericArray::clone_from_slice(bytes))) - } - - pub fn zeroizing_clone(&self) -> Zeroizing { - Zeroizing::new(SharedSymmetricKey(self.0)) - } - - pub fn digest(&self) -> Vec { - compute_digest::(self.as_bytes()).to_vec() - } - - pub fn as_bytes(&self) -> &[u8] { - self.0.as_slice() - } - - pub fn to_bytes(&self) -> Vec { - self.0.iter().copied().collect() - } - - pub fn try_from_base58_string>( - val: S, - ) -> Result { - let bs58_str = Zeroizing::new(val.into()); - let decoded = Zeroizing::new(bs58::decode(bs58_str).into_vec()?); - Self::try_from_bytes(&decoded) - } - - pub fn to_base58_string(&self) -> String { - let bytes = Zeroizing::new(self.to_bytes()); - bs58::encode(bytes).into_string() - } - - pub fn encrypt( - &self, - plaintext: &[u8], - nonce: &Nonce, - ) -> Result, SharedKeyUsageError> { - aead::encrypt::(&self.0, nonce, plaintext).map_err(Into::into) - } - - pub fn decrypt( - &self, - ciphertext: &[u8], - nonce: &Nonce, - ) -> Result, SharedKeyUsageError> { - aead::decrypt::(&self.0, nonce, ciphertext).map_err(Into::into) - } -} - -impl PemStorableKey for SharedSymmetricKey { - type Error = SharedKeyConversionError; - - fn pem_type() -> &'static str { - "AES-256-GCM-SIV GATEWAY SHARED KEY" - } - - fn to_bytes(&self) -> Vec { - self.to_bytes() - } - - fn from_bytes(bytes: &[u8]) -> Result { - Self::try_from_bytes(bytes) - } -} diff --git a/common/gateway-requests/src/types/binary_request.rs b/common/gateway-requests/src/types/binary_request.rs index b52ee819cd7..a0e75385f94 100644 --- a/common/gateway-requests/src/types/binary_request.rs +++ b/common/gateway-requests/src/types/binary_request.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::types::helpers::BinaryData; -use crate::{GatewayRequestsError, SharedGatewayKey}; +use crate::{GatewayRequestsError, SharedSymmetricKey}; use nym_sphinx::forwarding::packet::MixPacket; use strum::FromRepr; use tungstenite::Message; @@ -57,14 +57,14 @@ impl BinaryRequest { pub fn try_from_encrypted_tagged_bytes( bytes: Vec, - shared_key: &SharedGatewayKey, + shared_key: &SharedSymmetricKey, ) -> Result { BinaryData::from_raw(&bytes, shared_key)?.into_request(shared_key) } pub fn into_encrypted_tagged_bytes( self, - shared_key: &SharedGatewayKey, + shared_key: &SharedSymmetricKey, ) -> Result, GatewayRequestsError> { let kind = self.kind(); @@ -78,7 +78,7 @@ impl BinaryRequest { pub fn into_ws_message( self, - shared_key: &SharedGatewayKey, + shared_key: &SharedSymmetricKey, ) -> Result { // all variants are currently encrypted let blob = match self { diff --git a/common/gateway-requests/src/types/binary_response.rs b/common/gateway-requests/src/types/binary_response.rs index d3652b64e4f..69ea76c23e2 100644 --- a/common/gateway-requests/src/types/binary_response.rs +++ b/common/gateway-requests/src/types/binary_response.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::types::helpers::BinaryData; -use crate::{GatewayRequestsError, SharedGatewayKey}; +use crate::{GatewayRequestsError, SharedSymmetricKey}; use strum::FromRepr; use tungstenite::Message; @@ -38,14 +38,14 @@ impl BinaryResponse { pub fn try_from_encrypted_tagged_bytes( bytes: Vec, - shared_key: &SharedGatewayKey, + shared_key: &SharedSymmetricKey, ) -> Result { BinaryData::from_raw(&bytes, shared_key)?.into_response(shared_key) } pub fn into_encrypted_tagged_bytes( self, - shared_key: &SharedGatewayKey, + shared_key: &SharedSymmetricKey, ) -> Result, GatewayRequestsError> { let kind = self.kind(); @@ -58,7 +58,7 @@ impl BinaryResponse { pub fn into_ws_message( self, - shared_key: &SharedGatewayKey, + shared_key: &SharedSymmetricKey, ) -> Result { // all variants are currently encrypted let blob = match self { diff --git a/common/gateway-requests/src/types/helpers.rs b/common/gateway-requests/src/types/helpers.rs index f3d7ac3a1c5..9f326b1600b 100644 --- a/common/gateway-requests/src/types/helpers.rs +++ b/common/gateway-requests/src/types/helpers.rs @@ -3,12 +3,12 @@ use crate::{ BinaryRequest, BinaryRequestKind, BinaryResponse, BinaryResponseKind, GatewayRequestsError, - SharedGatewayKey, + SharedSymmetricKey, }; use std::iter::once; // each binary message consists of the following structure (for non-legacy messages) -// KIND || ENC_FLAG || MAYBE_NONCE || CIPHERTEXT/PLAINTEXT +// KIND || ENC_FLAG || NONCE || CIPHERTEXT/PLAINTEXT // first byte is the kind of data to influence further serialisation/deseralisation // second byte is a flag indicating whether the content is encrypted // then it's followed by a pseudorandom nonce, assuming encryption is used @@ -16,43 +16,25 @@ use std::iter::once; pub struct BinaryData<'a> { kind: u8, encrypted: bool, - maybe_nonce: Option<&'a [u8]>, + nonce: &'a [u8], data: &'a [u8], } impl<'a> BinaryData<'a> { // serialises possibly encrypted data into bytes to be put on the wire - pub fn into_raw(self, legacy: bool) -> Vec { - if legacy { - return self.data.to_vec(); - } - - let i = once(self.kind).chain(once(if self.encrypted { 1 } else { 0 })); - if let Some(nonce) = self.maybe_nonce { - i.chain(nonce.iter().copied()) - .chain(self.data.iter().copied()) - .collect() - } else { - i.chain(self.data.iter().copied()).collect() - } + pub fn into_raw(self) -> Vec { + once(self.kind) + .chain(once(if self.encrypted { 1 } else { 0 })) + .chain(self.nonce.iter().copied()) + .chain(self.data.iter().copied()) + .collect() } // attempts to perform basic parsing on bytes received from the wire pub fn from_raw( raw: &'a [u8], - available_key: &SharedGatewayKey, + available_key: &SharedSymmetricKey, ) -> Result { - // if we're using legacy key, it's quite simple: - // it's always encrypted with no nonce and the request/response kind is always 1 - if available_key.is_legacy() { - return Ok(BinaryData { - kind: 1, - encrypted: true, - maybe_nonce: None, - data: raw, - }); - } - if raw.len() < 2 { return Err(GatewayRequestsError::TooShortRequest); } @@ -74,7 +56,7 @@ impl<'a> BinaryData<'a> { Ok(BinaryData { kind, encrypted, - maybe_nonce: Some(&raw[2..2 + available_key.nonce_size()]), + nonce: &raw[2..2 + available_key.nonce_size()], data: &raw[2 + available_key.nonce_size()..], }) } @@ -83,30 +65,32 @@ impl<'a> BinaryData<'a> { pub fn make_encrypted_blob( kind: u8, plaintext: &[u8], - key: &SharedGatewayKey, + key: &SharedSymmetricKey, ) -> Result, GatewayRequestsError> { - let maybe_nonce = key.random_nonce_or_zero_iv(); + let nonce = key.random_nonce(); - let ciphertext = key.encrypt(plaintext, maybe_nonce.as_deref())?; + let ciphertext = key.encrypt(plaintext, &nonce)?; Ok(BinaryData { kind, encrypted: true, - maybe_nonce: maybe_nonce.as_deref(), + nonce: &nonce, data: &ciphertext, } - .into_raw(key.is_legacy())) + .into_raw()) } // attempts to parse previously recovered bytes into a [`BinaryRequest`] pub fn into_request( self, - key: &SharedGatewayKey, + key: &SharedSymmetricKey, ) -> Result { let kind = BinaryRequestKind::from_repr(self.kind) .ok_or(GatewayRequestsError::UnknownRequestKind { kind: self.kind })?; let plaintext = if self.encrypted { - &*key.decrypt(self.data, self.maybe_nonce)? + let nonce = SharedSymmetricKey::validate_aead_nonce(self.nonce)?; + + &*key.decrypt(self.data, &nonce)? } else { self.data }; @@ -117,13 +101,15 @@ impl<'a> BinaryData<'a> { // attempts to parse previously recovered bytes into a [`BinaryResponse`] pub fn into_response( self, - key: &SharedGatewayKey, + key: &SharedSymmetricKey, ) -> Result { let kind = BinaryResponseKind::from_repr(self.kind) .ok_or(GatewayRequestsError::UnknownResponseKind { kind: self.kind })?; let plaintext = if self.encrypted { - &*key.decrypt(self.data, self.maybe_nonce)? + let nonce = SharedSymmetricKey::validate_aead_nonce(self.nonce)?; + + &*key.decrypt(self.data, &nonce)? } else { self.data }; diff --git a/common/gateway-requests/src/types/registration_handshake_wrapper.rs b/common/gateway-requests/src/types/registration_handshake_wrapper.rs index be75af675be..2097fc6800f 100644 --- a/common/gateway-requests/src/types/registration_handshake_wrapper.rs +++ b/common/gateway-requests/src/types/registration_handshake_wrapper.rs @@ -10,7 +10,7 @@ use std::str::FromStr; pub enum RegistrationHandshake { HandshakePayload { #[serde(default)] - protocol_version: Option, + protocol_version: GatewayProtocolVersion, data: Vec, }, HandshakeError { @@ -19,7 +19,7 @@ pub enum RegistrationHandshake { } impl RegistrationHandshake { - pub fn new_payload(data: Vec, protocol_version: Option) -> Self { + pub fn new_payload(data: Vec, protocol_version: GatewayProtocolVersion) -> Self { RegistrationHandshake::HandshakePayload { protocol_version, data, @@ -66,7 +66,7 @@ mod tests { fn handshake_payload_can_be_deserialized_into_register_handshake_init_request() { let handshake_data = vec![1, 2, 3, 4, 5, 6]; let handshake_payload_with_protocol = RegistrationHandshake::HandshakePayload { - protocol_version: Some(42), + protocol_version: 42, data: handshake_data.clone(), }; let serialized = serde_json::to_string(&handshake_payload_with_protocol).unwrap(); @@ -77,25 +77,7 @@ mod tests { protocol_version, data, } => { - assert_eq!(protocol_version, Some(42)); - assert_eq!(data, handshake_data) - } - _ => panic!("this branch shouldn't have been reached!"), - } - - let handshake_payload_without_protocol = RegistrationHandshake::HandshakePayload { - protocol_version: None, - data: handshake_data.clone(), - }; - let serialized = serde_json::to_string(&handshake_payload_without_protocol).unwrap(); - let deserialized = ClientControlRequest::try_from(serialized).unwrap(); - - match deserialized { - ClientControlRequest::RegisterHandshakeInitRequest { - protocol_version, - data, - } => { - assert!(protocol_version.is_none()); + assert_eq!(protocol_version, 42); assert_eq!(data, handshake_data) } _ => panic!("this branch shouldn't have been reached!"), diff --git a/common/gateway-requests/src/types/text_request/authenticate.rs b/common/gateway-requests/src/types/text_request/authenticate.rs index 65dacb3f00b..0e44becec3d 100644 --- a/common/gateway-requests/src/types/text_request/authenticate.rs +++ b/common/gateway-requests/src/types/text_request/authenticate.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::{ - AuthenticationFailure, GatewayProtocolVersion, GatewayRequestsError, SharedGatewayKey, + AuthenticationFailure, GatewayProtocolVersion, GatewayRequestsError, SharedSymmetricKey, }; use nym_crypto::asymmetric::ed25519; use serde::{Deserialize, Serialize}; @@ -23,7 +23,7 @@ pub struct AuthenticateRequest { impl AuthenticateRequest { pub fn new( protocol_version: GatewayProtocolVersion, - shared_key: &SharedGatewayKey, + shared_key: &SharedSymmetricKey, identity_keys: &ed25519::KeyPair, ) -> Result { let content = AuthenticateRequestContent::new( @@ -72,14 +72,14 @@ impl AuthenticateRequest { pub fn verify_ciphertext( &self, - shared_key: &SharedGatewayKey, + shared_key: &SharedSymmetricKey, ) -> Result<(), AuthenticationFailure> { let expected = shared_key.encrypt( self.content .client_identity .derive_destination_address() .as_bytes_ref(), - Some(&self.content.nonce), + &SharedSymmetricKey::validate_aead_nonce(&self.content.nonce)?, )?; if !bool::from(expected.ct_eq(&self.content.address_ciphertext)) { @@ -117,20 +117,19 @@ pub struct AuthenticateRequestContent { impl AuthenticateRequestContent { fn new( protocol_version: u8, - shared_key: &SharedGatewayKey, + shared_key: &SharedSymmetricKey, client_identity: ed25519::PublicKey, ) -> Result { - let nonce = shared_key.random_nonce_or_iv(); + let nonce = shared_key.random_nonce(); let destination_address = client_identity.derive_destination_address(); - let address_ciphertext = - shared_key.encrypt(destination_address.as_bytes_ref(), Some(&nonce))?; + let address_ciphertext = shared_key.encrypt(destination_address.as_bytes_ref(), &nonce)?; let now = OffsetDateTime::now_utc(); Ok(AuthenticateRequestContent { protocol_version, client_identity, address_ciphertext, - nonce, + nonce: nonce.to_vec(), request_unix_timestamp: now.unix_timestamp() as u64, // SAFETY: we're running this in post 1970... }) } diff --git a/common/gateway-requests/src/types/text_request/mod.rs b/common/gateway-requests/src/types/text_request/mod.rs index b15a27bf935..f672eefd55c 100644 --- a/common/gateway-requests/src/types/text_request/mod.rs +++ b/common/gateway-requests/src/types/text_request/mod.rs @@ -3,13 +3,9 @@ use crate::models::CredentialSpendingRequest; use crate::text_request::authenticate::AuthenticateRequest; -use crate::{ - GatewayProtocolVersion, GatewayRequestsError, SharedGatewayKey, SymmetricKey, - AES_GCM_SIV_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, -}; +use crate::{GatewayProtocolVersion, GatewayRequestsError, SharedSymmetricKey}; use nym_credentials_interface::CredentialSpendingData; use nym_crypto::asymmetric::ed25519; -use nym_sphinx::DestinationAddressBytes; use nym_statistics_common::types::SessionType; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -21,23 +17,14 @@ pub mod authenticate; #[derive(Serialize, Deserialize, Debug, Clone)] #[non_exhaustive] pub enum ClientRequest { - UpgradeKey { - hkdf_salt: Vec, - derived_key_digest: Vec, - }, - ForgetMe { - client: bool, - stats: bool, - }, - RememberMe { - session_type: SessionType, - }, + ForgetMe { client: bool, stats: bool }, + RememberMe { session_type: SessionType }, } impl ClientRequest { - pub fn encrypt( + pub fn encrypt( &self, - key: &S, + key: &SharedSymmetricKey, ) -> Result { // we're using json representation for few reasons: // - ease of re-implementation in other languages (compared to for example bincode) @@ -47,17 +34,21 @@ impl ClientRequest { // 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))?; - Ok(ClientControlRequest::EncryptedRequest { ciphertext, nonce }) + let nonce = key.random_nonce(); + let ciphertext = key.encrypt(&plaintext, &nonce)?; + Ok(ClientControlRequest::EncryptedRequest { + ciphertext, + nonce: nonce.to_vec(), + }) } - pub fn decrypt( + pub fn decrypt( ciphertext: &[u8], nonce: &[u8], - key: &S, + key: &SharedSymmetricKey, ) -> Result { - let plaintext = key.decrypt(ciphertext, Some(nonce))?; + let nonce = SharedSymmetricKey::validate_aead_nonce(nonce)?; + let plaintext = key.decrypt(ciphertext, &nonce)?; serde_json::from_slice(&plaintext) .map_err(|source| GatewayRequestsError::MalformedRequest { source }) } @@ -68,35 +59,20 @@ impl ClientRequest { #[serde(tag = "type", rename_all = "camelCase")] #[non_exhaustive] pub enum ClientControlRequest { - // TODO: should this also contain a MAC considering that at this point we already - // have the shared key derived? - Authenticate { - #[serde(default)] - protocol_version: Option, - address: String, - enc_address: String, - iv: String, - }, - AuthenticateV2(Box), #[serde(alias = "handshakePayload")] RegisterHandshakeInitRequest { #[serde(default)] - protocol_version: Option, + protocol_version: GatewayProtocolVersion, data: Vec, }, - BandwidthCredential { - enc_credential: Vec, - iv: Vec, - }, - BandwidthCredentialV2 { - enc_credential: Vec, - iv: Vec, - }, EcashCredential { enc_credential: Vec, - iv: Vec, + // Old gateways only understand `iv` so rename the field, but have nonce as an alias for next version update, to then phase out `iv` + #[serde(rename = "iv")] + #[serde(alias = "nonce")] + nonce: Vec, }, UpgradeModeJWT { // no need to encrypt it as it's public anyway @@ -109,40 +85,29 @@ pub enum ClientControlRequest { }, SupportedProtocol {}, // if you're adding new variants here, consider putting them inside `ClientRequest` instead + + // NO LONGER SUPPORTED + Authenticate { + #[serde(default)] + protocol_version: Option, + address: String, + enc_address: String, + iv: String, + }, + + BandwidthCredential { + enc_credential: Vec, + iv: Vec, + }, + BandwidthCredentialV2 { + enc_credential: Vec, + iv: Vec, + }, } impl ClientControlRequest { - pub fn new_legacy_authenticate( - address: DestinationAddressBytes, - shared_key: &SharedGatewayKey, - uses_credentials: bool, - ) -> Result { - // 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 { - Some(CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION) - } else { - // if we're not going to be using credentials, advertise lower protocol version to allow connection - // to wider range of gateways - Some(INITIAL_PROTOCOL_VERSION) - }; - - let nonce = shared_key.random_nonce_or_iv(); - let ciphertext = shared_key.encrypt_naive(address.as_bytes_ref(), Some(&nonce))?; - - Ok(ClientControlRequest::Authenticate { - protocol_version, - address: address.as_base58_string(), - enc_address: bs58::encode(&ciphertext).into_string(), - iv: bs58::encode(&nonce).into_string(), - }) - } - pub fn new_authenticate_v2( - shared_key: &SharedGatewayKey, + shared_key: &SharedSymmetricKey, identity_keys: &ed25519::KeyPair, protocol_version: GatewayProtocolVersion, ) -> Result { @@ -174,26 +139,27 @@ impl ClientControlRequest { pub fn new_enc_ecash_credential( credential: CredentialSpendingData, - shared_key: &SharedGatewayKey, + shared_key: &SharedSymmetricKey, ) -> Result { let cred = CredentialSpendingRequest::new(credential); let serialized_credential = cred.to_bytes(); - let nonce = shared_key.random_nonce_or_iv(); - let enc_credential = shared_key.encrypt(&serialized_credential, Some(&nonce))?; + let nonce = shared_key.random_nonce(); + let enc_credential = shared_key.encrypt(&serialized_credential, &nonce)?; Ok(ClientControlRequest::EcashCredential { enc_credential, - iv: nonce, + nonce: nonce.to_vec(), }) } pub fn try_from_enc_ecash_credential( enc_credential: Vec, - shared_key: &SharedGatewayKey, - iv: Vec, + shared_key: &SharedSymmetricKey, + nonce: Vec, ) -> Result { - let credential_bytes = shared_key.decrypt(&enc_credential, Some(&iv))?; + let nonce = SharedSymmetricKey::validate_aead_nonce(&nonce)?; + let credential_bytes = shared_key.decrypt(&enc_credential, &nonce)?; CredentialSpendingRequest::try_from_bytes(credential_bytes.as_slice()) .map_err(|_| GatewayRequestsError::MalformedEncryption) } diff --git a/common/gateway-requests/src/types/text_response.rs b/common/gateway-requests/src/types/text_response.rs index 140aa26bbd7..828c55c15fc 100644 --- a/common/gateway-requests/src/types/text_response.rs +++ b/common/gateway-requests/src/types/text_response.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ - GatewayProtocolVersion, GatewayRequestsError, SimpleGatewayRequestsError, SymmetricKey, + GatewayProtocolVersion, GatewayRequestsError, SharedSymmetricKey, SimpleGatewayRequestsError, }; use serde::{Deserialize, Serialize}; use tungstenite::Message; @@ -12,15 +12,14 @@ use tungstenite::Message; #[derive(Serialize, Deserialize, Debug)] #[non_exhaustive] pub enum SensitiveServerResponse { - KeyUpgradeAck {}, ForgetMeAck {}, RememberMeAck {}, } impl SensitiveServerResponse { - pub fn encrypt( + pub fn encrypt( &self, - key: &S, + key: &SharedSymmetricKey, ) -> Result { // we're using json representation for few reasons: // - ease of re-implementation in other languages (compared to for example bincode) @@ -30,17 +29,21 @@ impl SensitiveServerResponse { // 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))?; - Ok(ServerResponse::EncryptedResponse { ciphertext, nonce }) + let nonce = key.random_nonce(); + let ciphertext = key.encrypt(&plaintext, &nonce)?; + Ok(ServerResponse::EncryptedResponse { + ciphertext, + nonce: nonce.to_vec(), + }) } - pub fn decrypt( + pub fn decrypt( ciphertext: &[u8], nonce: &[u8], - key: &S, + key: &SharedSymmetricKey, ) -> Result { - let plaintext = key.decrypt(ciphertext, Some(nonce))?; + let nonce = SharedSymmetricKey::validate_aead_nonce(nonce)?; + let plaintext = key.decrypt(ciphertext, &nonce)?; serde_json::from_slice(&plaintext) .map_err(|source| GatewayRequestsError::MalformedRequest { source }) } @@ -72,7 +75,7 @@ pub struct SendResponse { pub enum ServerResponse { Authenticate { #[serde(default)] - protocol_version: Option, + protocol_version: GatewayProtocolVersion, status: bool, bandwidth_remaining: i64, @@ -83,7 +86,7 @@ pub enum ServerResponse { }, Register { #[serde(default)] - protocol_version: Option, + protocol_version: GatewayProtocolVersion, status: bool, /// Flag indicating whether the gateway has detected the system is undergoing the upgrade diff --git a/common/gateway-storage/migrations/20251126120000_remove_aes128ctr_key.sql b/common/gateway-storage/migrations/20251126120000_remove_aes128ctr_key.sql new file mode 100644 index 00000000000..3ecb33ebda7 --- /dev/null +++ b/common/gateway-storage/migrations/20251126120000_remove_aes128ctr_key.sql @@ -0,0 +1,23 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + + +-- make aes256gcm column non-nullable and drop any clients that still use the legacy keys +CREATE TABLE shared_keys_tmp +( + client_id INTEGER NOT NULL PRIMARY KEY REFERENCES clients (id), + client_address_bs58 TEXT NOT NULL UNIQUE, + derived_aes256_gcm_siv_key BLOB NOT NULL, + last_used_authentication TIMESTAMP WITHOUT TIME ZONE +); + +INSERT INTO shared_keys_tmp (client_id, client_address_bs58, derived_aes256_gcm_siv_key, last_used_authentication) +SELECT client_id, client_address_bs58, derived_aes256_gcm_siv_key, last_used_authentication +FROM shared_keys +WHERE derived_aes256_gcm_siv_key IS NOT NULL; + +DROP TABLE shared_keys; +ALTER TABLE shared_keys_tmp + RENAME TO shared_keys; \ No newline at end of file diff --git a/common/gateway-storage/src/lib.rs b/common/gateway-storage/src/lib.rs index 0a3144ac13e..58708b8aef7 100644 --- a/common/gateway-storage/src/lib.rs +++ b/common/gateway-storage/src/lib.rs @@ -9,7 +9,7 @@ use models::{ VerifiedTicket, WireguardPeer, }; use nym_credentials_interface::ClientTicket; -use nym_gateway_requests::shared_key::SharedGatewayKey; +use nym_gateway_requests::shared_key::SharedSymmetricKey; use nym_sphinx::DestinationAddressBytes; use shared_keys::SharedKeysManager; use sqlx::{ @@ -165,7 +165,7 @@ impl SharedKeyGatewayStorage for GatewayStorage { async fn insert_shared_keys( &self, client_address: DestinationAddressBytes, - shared_keys: &SharedGatewayKey, + shared_keys: &SharedSymmetricKey, ) -> Result { let client_address_bs58 = client_address.as_base58_string(); let client_id = match self @@ -184,8 +184,7 @@ impl SharedKeyGatewayStorage for GatewayStorage { .insert_shared_keys( client_id, client_address_bs58, - shared_keys.aes128_ctr_hmac_bs58().as_deref(), - shared_keys.aes256_gcm_siv().as_deref(), + shared_keys.to_bytes().as_ref(), ) .await?; Ok(client_id) diff --git a/common/gateway-storage/src/models.rs b/common/gateway-storage/src/models.rs index 32b12b80a50..d982f2ec282 100644 --- a/common/gateway-storage/src/models.rs +++ b/common/gateway-storage/src/models.rs @@ -3,7 +3,7 @@ use crate::{error::GatewayStorageError, make_bincode_serializer}; use nym_credentials_interface::{AvailableBandwidth, ClientTicket, CredentialSpendingData}; -use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey}; +use nym_gateway_requests::shared_key::SharedSymmetricKey; use sqlx::FromRow; use time::OffsetDateTime; @@ -18,33 +18,16 @@ pub struct PersistedSharedKeys { #[allow(dead_code)] pub client_address_bs58: String, - pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option, - pub derived_aes256_gcm_siv_key: Option>, + pub derived_aes256_gcm_siv_key: Vec, pub last_used_authentication: Option, } -impl TryFrom for SharedGatewayKey { +impl TryFrom for SharedSymmetricKey { type Error = GatewayStorageError; fn try_from(value: PersistedSharedKeys) -> Result { - match ( - &value.derived_aes256_gcm_siv_key, - &value.derived_aes128_ctr_blake3_hmac_keys_bs58, - ) { - (None, None) => Err(GatewayStorageError::MissingSharedKey { - id: value.client_id, - }), - (Some(aes256gcm_siv), _) => { - let current_key = SharedSymmetricKey::try_from_bytes(aes256gcm_siv) - .map_err(|source| GatewayStorageError::DataCorruption(source.to_string()))?; - Ok(SharedGatewayKey::Current(current_key)) - } - (None, Some(aes128ctr_hmac)) => { - let legacy_key = LegacySharedKeys::try_from_base58_string(aes128ctr_hmac) - .map_err(|source| GatewayStorageError::DataCorruption(source.to_string()))?; - Ok(SharedGatewayKey::Legacy(legacy_key)) - } - } + SharedSymmetricKey::try_from_bytes(&value.derived_aes256_gcm_siv_key) + .map_err(|source| GatewayStorageError::DataCorruption(source.to_string())) } } diff --git a/common/gateway-storage/src/shared_keys.rs b/common/gateway-storage/src/shared_keys.rs index a7ae832cac2..1214d062d36 100644 --- a/common/gateway-storage/src/shared_keys.rs +++ b/common/gateway-storage/src/shared_keys.rs @@ -42,26 +42,22 @@ impl SharedKeysManager { &self, client_id: i64, client_address_bs58: String, - derived_aes128_ctr_blake3_hmac_keys_bs58: Option<&String>, - derived_aes256_gcm_siv_key: Option<&Vec>, + derived_aes256_gcm_siv_key: &Vec, ) -> Result<(), sqlx::Error> { // https://stackoverflow.com/a/20310838 // we don't want to be using `INSERT OR REPLACE INTO` due to the foreign key on `available_bandwidth` if the entry already exists sqlx::query!( r#" - INSERT OR IGNORE INTO shared_keys(client_id, client_address_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key) VALUES (?, ?, ?, ?); + INSERT OR IGNORE INTO shared_keys(client_id, client_address_bs58,derived_aes256_gcm_siv_key) VALUES (?, ?, ?); UPDATE shared_keys SET - derived_aes128_ctr_blake3_hmac_keys_bs58 = ?, derived_aes256_gcm_siv_key = ? WHERE client_address_bs58 = ? "#, client_id, client_address_bs58, - derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key, - derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key, client_address_bs58, ).execute(&self.connection_pool).await?; diff --git a/common/gateway-storage/src/traits.rs b/common/gateway-storage/src/traits.rs index fcd113a293d..f3a8efcdf80 100644 --- a/common/gateway-storage/src/traits.rs +++ b/common/gateway-storage/src/traits.rs @@ -3,7 +3,7 @@ use async_trait::async_trait; use nym_credentials_interface::ClientTicket; -use nym_gateway_requests::SharedGatewayKey; +use nym_gateway_requests::SharedSymmetricKey; use nym_sphinx::DestinationAddressBytes; use time::OffsetDateTime; @@ -25,7 +25,7 @@ pub trait SharedKeyGatewayStorage { async fn insert_shared_keys( &self, client_address: DestinationAddressBytes, - shared_keys: &SharedGatewayKey, + shared_keys: &SharedSymmetricKey, ) -> Result; async fn get_shared_keys( &self, diff --git a/common/topology/src/node.rs b/common/topology/src/node.rs index c542d93e04b..85cb81b51f0 100644 --- a/common/topology/src/node.rs +++ b/common/topology/src/node.rs @@ -89,6 +89,45 @@ impl RoutingNode { self.ws_entry_address_no_tls(prefer_ipv6) } + pub fn ws_entry_address_with_fallback( + &self, + prefer_ipv6: bool, + no_hostname: bool, + ) -> (Option, Option) { + let Some(entry) = &self.entry else { + return (None, None); + }; + + // Put hostname first if we want it + let maybe_hostname = if !no_hostname { + entry.hostname.clone() + } else { + None + }; + + // Put ipv6 first or keep them as is + let ips: Vec<&IpAddr> = if prefer_ipv6 { + entry + .ip_addresses + .iter() + .filter(|ip| ip.is_ipv6()) + .chain(entry.ip_addresses.iter().filter(|ip| ip.is_ipv4())) + .collect() + } else { + entry.ip_addresses.iter().collect() + }; + + // chain everything and keep the top two as ws addresses + let ws_addresses: Vec<_> = maybe_hostname + .into_iter() + .chain(ips.into_iter().map(|ip| ip.to_string())) + .take(2) + .map(|host| format!("ws://{host}:{}", entry.clients_ws_port)) + .collect(); + + (ws_addresses.first().cloned(), ws_addresses.get(1).cloned()) + } + pub fn identity(&self) -> ed25519::PublicKey { self.identity_key } diff --git a/common/wasm/client-core/src/helpers.rs b/common/wasm/client-core/src/helpers.rs index de5cd6883a2..8d7c4dc6e4d 100644 --- a/common/wasm/client-core/src/helpers.rs +++ b/common/wasm/client-core/src/helpers.rs @@ -139,7 +139,7 @@ pub async fn setup_gateway_wasm( GatewaySetup::MustLoad { gateway_id: None } } else { let selection_spec = - GatewaySelectionSpecification::new(chosen_gateway.clone(), None, force_tls); + GatewaySelectionSpecification::new(chosen_gateway.clone(), None, force_tls, false); GatewaySetup::New { specification: selection_spec, @@ -218,6 +218,7 @@ pub async fn add_gateway( preferred_gateway.clone(), latency_based_selection, force_tls, + false, ); let preferred_gateway = preferred_gateway diff --git a/common/wasm/client-core/src/storage/core_client_traits.rs b/common/wasm/client-core/src/storage/core_client_traits.rs index 8d08af59c7d..3421292d727 100644 --- a/common/wasm/client-core/src/storage/core_client_traits.rs +++ b/common/wasm/client-core/src/storage/core_client_traits.rs @@ -8,15 +8,15 @@ use crate::storage::wasm_client_traits::WasmClientStorage; use crate::storage::ClientStorage; use async_trait::async_trait; use nym_client_core::client::base_client::storage::{ - gateways_storage::{ActiveGateway, GatewayRegistration, GatewaysDetailsStore}, + gateways_storage::{ + ActiveGateway, GatewayPublishedData, GatewayRegistration, GatewaysDetailsStore, + }, MixnetClientStorage, }; use nym_client_core::client::key_manager::persistence::KeyStore; use nym_client_core::client::key_manager::ClientKeys; use nym_client_core::client::replies::reply_storage::browser_backend; use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage; -use nym_crypto::asymmetric::ed25519::PublicKey; -use nym_gateway_client::SharedSymmetricKey; use wasm_utils::console_log; // temporary until other variants are properly implemented (probably it should get changed into `ClientStorage` @@ -156,17 +156,16 @@ impl GatewaysDetailsStore for ClientStorage { self.store_registered_gateway(&raw_registration).await } - async fn upgrade_stored_remote_gateway_key( + async fn update_gateway_published_data( &self, - gateway_id: PublicKey, - updated_key: &SharedSymmetricKey, + gateway_id: &str, + published_data: &GatewayPublishedData, ) -> Result<(), Self::StorageError> { - self.update_remote_gateway_key( - &gateway_id.to_base58_string(), - None, - Some(updated_key.as_bytes()), - ) - .await + // Get gateway and update it + let mut gateway = self.must_get_registered_gateway(gateway_id).await?; + gateway.published_data = published_data.into(); + // Store it again, key didn't change + self.store_gateway_details(&gateway.try_into()?).await } async fn remove_gateway_details(&self, gateway_id: &str) -> Result<(), Self::StorageError> { diff --git a/common/wasm/client-core/src/storage/types.rs b/common/wasm/client-core/src/storage/types.rs index b94f04f6514..959add60e07 100644 --- a/common/wasm/client-core/src/storage/types.rs +++ b/common/wasm/client-core/src/storage/types.rs @@ -2,11 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 use nym_client_core::client::base_client::storage::gateways_storage::{ - BadGateway, GatewayDetails, GatewayRegistration, RawRemoteGatewayDetails, RemoteGatewayDetails, + BadGateway, GatewayDetails, GatewayPublishedData, GatewayRegistration, RawGatewayPublishedData, + RawRemoteGatewayDetails, RemoteGatewayDetails, }; -use nym_gateway_client::SharedGatewayKey; use serde::{Deserialize, Serialize}; -use std::ops::Deref; use time::OffsetDateTime; use zeroize::Zeroize; @@ -18,14 +17,11 @@ pub struct WasmRawRegisteredGateway { #[zeroize(skip)] pub registration_timestamp: OffsetDateTime, - pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option, - #[serde(default)] - pub derived_aes256_gcm_siv_key: Option>, - - pub gateway_owner_address: Option, + pub derived_aes256_gcm_siv_key: Vec, - pub gateway_listener: String, + #[zeroize(skip)] + pub published_data: WasmRawGatewayPublishedData, } impl TryFrom for GatewayRegistration { @@ -35,11 +31,12 @@ impl TryFrom for GatewayRegistration { // offload some parsing to an existing impl let raw_remote = RawRemoteGatewayDetails { gateway_id_bs58: value.gateway_id_bs58, - derived_aes128_ctr_blake3_hmac_keys_bs58: value - .derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key: value.derived_aes256_gcm_siv_key, - gateway_owner_address: value.gateway_owner_address, - gateway_listener: value.gateway_listener, + published_data: RawGatewayPublishedData { + gateway_listener: value.published_data.gateway_listener, + fallback_listener: value.published_data.fallback_listener, + expiration_timestamp: value.published_data.expiration_timestamp, + }, }; let remote: RemoteGatewayDetails = raw_remote.try_into()?; @@ -56,22 +53,32 @@ impl<'a> From<&'a GatewayRegistration> for WasmRawRegisteredGateway { panic!("somehow obtained custom gateway registration in wasm!") }; - let (derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key) = - match remote_details.shared_key.deref() { - SharedGatewayKey::Current(key) => (None, Some(key.to_bytes())), - SharedGatewayKey::Legacy(key) => (Some(key.to_base58_string()), None), - }; + let derived_aes256_gcm_siv_key = remote_details.shared_key.to_bytes().to_vec(); WasmRawRegisteredGateway { gateway_id_bs58: remote_details.gateway_id.to_string(), registration_timestamp: value.registration_timestamp, - derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key, - gateway_listener: remote_details.gateway_listener.to_string(), - gateway_owner_address: remote_details - .gateway_owner_address - .as_ref() - .map(|a| a.to_string()), + published_data: (&remote_details.published_data).into(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WasmRawGatewayPublishedData { + pub gateway_listener: String, + + pub fallback_listener: Option, + + pub expiration_timestamp: OffsetDateTime, +} + +impl<'a> From<&'a GatewayPublishedData> for WasmRawGatewayPublishedData { + fn from(value: &'a GatewayPublishedData) -> Self { + WasmRawGatewayPublishedData { + gateway_listener: value.listeners.primary.to_string(), + fallback_listener: value.listeners.fallback.as_ref().map(|uri| uri.to_string()), + expiration_timestamp: value.expiration_timestamp, } } } diff --git a/common/wasm/client-core/src/storage/wasm_client_traits.rs b/common/wasm/client-core/src/storage/wasm_client_traits.rs index 8cc7edc87ae..916d22cd1db 100644 --- a/common/wasm/client-core/src/storage/wasm_client_traits.rs +++ b/common/wasm/client-core/src/storage/wasm_client_traits.rs @@ -10,7 +10,6 @@ use std::error::Error; use thiserror::Error; use wasm_bindgen::JsValue; use wasm_storage::traits::BaseWasmStorage; -use zeroize::Zeroize; // v1 tables pub(crate) mod v1 { @@ -238,23 +237,6 @@ pub trait WasmClientStorage: BaseWasmStorage { .map_err(Into::into) } - async fn update_remote_gateway_key( - &self, - gateway_id_bs58: &str, - derived_aes128_ctr_blake3_hmac_keys_bs58: Option<&str>, - derived_aes256_gcm_siv_key: Option<&[u8]>, - ) -> Result<(), ::StorageError> { - if let Some(mut current) = self.maybe_get_registered_gateway(gateway_id_bs58).await? { - current.derived_aes128_ctr_blake3_hmac_keys_bs58 = - derived_aes128_ctr_blake3_hmac_keys_bs58.map(|k| k.to_string()); - current.derived_aes256_gcm_siv_key = derived_aes256_gcm_siv_key.map(|k| k.to_vec()); - self.store_registered_gateway(¤t).await?; - current.zeroize(); - } - - Ok(()) - } - async fn remove_registered_gateway( &self, gateway_id: &str, diff --git a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs index b40642930a8..189a5a4ca66 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -27,7 +27,6 @@ use nym_gateway_requests::{ }; use nym_gateway_storage::error::GatewayStorageError; use nym_gateway_storage::traits::BandwidthGatewayStorage; -use nym_gateway_storage::traits::SharedKeyGatewayStorage; use nym_node_metrics::events::MetricsEvent; use nym_sphinx::forwarding::packet::MixPacket; use nym_statistics_common::{gateways::GatewaySessionEvent, types::SessionType}; @@ -65,7 +64,7 @@ pub enum RequestHandlingError { #[error("failed to decrypt provided text request")] InvalidEncryptedTextRequest, - #[error("Provided binary request was malformed - {0}")] + #[error("Provided text request was malformed - {0}")] InvalidTextRequest(>::Error), #[error("The received request is not valid in the current context: {additional_context}")] @@ -410,35 +409,6 @@ impl AuthenticatedHandler { Ok(SensitiveServerResponse::RememberMeAck {}.encrypt(&self.client.shared_keys)?) } - async fn handle_key_upgrade( - &mut self, - hkdf_salt: Vec, - client_key_digest: Vec, - ) -> Result { - if !self.client.shared_keys.is_legacy() { - return Ok(ServerResponse::new_error( - "the connection is already using an aes256-gcm-siv key", - )); - } - let legacy_key = self.client.shared_keys.unwrap_legacy(); - let Some(upgraded_key) = legacy_key.upgrade_verify(&hkdf_salt, &client_key_digest) else { - return Ok(ServerResponse::new_error( - "failed to derive matching aes256-gcm-siv key", - )); - }; - - let updated_key = upgraded_key.into(); - self.inner - .shared_state - .storage - .insert_shared_keys(self.client.address, &updated_key) - .await?; - - // swap the in-memory key - self.client.shared_keys = updated_key; - Ok(SensitiveServerResponse::KeyUpgradeAck {}.encrypt(&self.client.shared_keys)?) - } - async fn handle_encrypted_text_request( &mut self, ciphertext: Vec, @@ -449,10 +419,6 @@ impl AuthenticatedHandler { }; match req { - ClientRequest::UpgradeKey { - hkdf_salt, - derived_key_digest, - } => self.handle_key_upgrade(hkdf_salt, derived_key_digest).await, ClientRequest::ForgetMe { client, stats } => self.handle_forget_me(client, stats).await, ClientRequest::RememberMe { session_type } => { self.handle_remember_me(session_type).await @@ -489,9 +455,10 @@ impl AuthenticatedHandler { ClientControlRequest::EncryptedRequest { ciphertext, nonce } => { self.handle_encrypted_text_request(ciphertext, nonce).await } - ClientControlRequest::EcashCredential { enc_credential, iv } => { - self.handle_ecash_bandwidth(enc_credential, iv).await - } + ClientControlRequest::EcashCredential { + enc_credential, + nonce, + } => self.handle_ecash_bandwidth(enc_credential, nonce).await, ClientControlRequest::UpgradeModeJWT { token } => { self.handle_upgrade_mode_jwt(token).await } diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index ead0fb18f37..7547250dd1d 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -17,15 +17,12 @@ use nym_credentials_interface::{AvailableBandwidth, DEFAULT_MIXNET_REQUEST_BANDW use nym_crypto::aes::cipher::crypto_common::rand_core::RngCore; use nym_crypto::asymmetric::ed25519; use nym_gateway_requests::authenticate::AuthenticateRequest; -use nym_gateway_requests::authentication::encrypted_address::{ - EncryptedAddressBytes, EncryptedAddressConversionError, -}; use nym_gateway_requests::registration::handshake::HandshakeResult; use nym_gateway_requests::{ registration::handshake::{error::HandshakeError, gateway_handshake}, types::{ClientControlRequest, ServerResponse}, AuthenticationFailure, BinaryResponse, GatewayProtocolVersion, GatewayProtocolVersionExt, - SharedGatewayKey, CURRENT_PROTOCOL_VERSION, + SharedSymmetricKey, CURRENT_PROTOCOL_VERSION, }; use nym_gateway_storage::error::GatewayStorageError; use nym_gateway_storage::traits::BandwidthGatewayStorage; @@ -50,6 +47,9 @@ pub(crate) enum InitialAuthenticationError { #[error(transparent)] AuthenticationFailure(#[from] AuthenticationFailure), + #[error("the legacy authentication method is no longer supported. please update your client")] + UnsupportedLegacyAuthentication, + #[error("attempted to overwrite client session with a stale authentication")] StaleSessionOverwrite, @@ -68,19 +68,9 @@ pub(crate) enum InitialAuthenticationError { #[error("Failed to perform registration handshake: {0}")] HandshakeError(#[from] HandshakeError), - #[error("Provided client address is malformed: {0}")] - // sphinx error is not used here directly as its messaging might be confusing to people - MalformedClientAddress(String), - - #[error("Provided encrypted client address is malformed: {0}")] - MalformedEncryptedAddress(#[from] EncryptedAddressConversionError), - #[error("There is already an open connection to this client")] DuplicateConnection, - #[error("provided authentication IV is malformed: {0}")] - MalformedIV(bs58::decode::Error), - #[error("Only 'Register' or 'Authenticate' requests are allowed")] InvalidRequest, @@ -90,6 +80,9 @@ pub(crate) enum InitialAuthenticationError { #[error("Experienced connection error: {0}")] ConnectionError(Box), + #[error("Attempted to negotiate connection with client using incompatible protocol version. Ours is {current} and the client reports {client}")] + IncompatibleProtocol { client: u8, current: u8 }, + #[error("failed to send authentication response: {source}")] ResponseSendFailure { #[source] @@ -192,7 +185,7 @@ impl FreshHandler { async fn perform_registration_handshake( &mut self, init_msg: Vec, - requested_protocol: Option, + requested_protocol: GatewayProtocolVersion, ) -> Result where S: AsyncRead + AsyncWrite + Unpin + Send, @@ -279,7 +272,7 @@ impl FreshHandler { #[allow(clippy::panic)] pub(crate) async fn push_packets_to_client( &mut self, - shared_keys: &SharedGatewayKey, + shared_keys: &SharedSymmetricKey, packets: Vec>, ) -> Result<(), WsError> where @@ -337,7 +330,7 @@ impl FreshHandler { async fn push_stored_messages_to_client( &mut self, client_address: DestinationAddressBytes, - shared_keys: &SharedGatewayKey, + shared_keys: &SharedSymmetricKey, ) -> Result<(), InitialAuthenticationError> where S: AsyncRead + AsyncWrite + Unpin, @@ -391,34 +384,6 @@ impl FreshHandler { Ok(Some(keys)) } - /// Checks whether the stored shared keys match the received data, i.e. whether the upon decryption - /// the provided encrypted address matches the expected unencrypted address. - /// - /// Returns the retrieved shared keys if the check was successful. - /// - /// # Arguments - /// - /// * `client_address`: address of the client. - /// * `encrypted_address`: encrypted address of the client, presumably encrypted using the shared keys. - /// * `iv`: nonce/iv created for this particular encryption. - async fn auth_v1_verify_stored_shared_key( - &self, - client_address: DestinationAddressBytes, - encrypted_address: EncryptedAddressBytes, - nonce: &[u8], - ) -> Result, InitialAuthenticationError> { - let Some(keys) = self.retrieve_shared_key(client_address).await? else { - return Ok(None); - }; - - // LEGACY ISSUE: we're not verifying HMAC key - if encrypted_address.verify(&client_address, &keys.key, nonce) { - Ok(Some(keys)) - } else { - Ok(None) - } - } - async fn handle_duplicate_client( &mut self, address: DestinationAddressBytes, @@ -508,24 +473,31 @@ impl FreshHandler { fn negotiate_proposed_protocol( &self, - client_protocol_version: Option, - ) -> Option { + client_protocol_version: GatewayProtocolVersion, + ) -> Result { + let incompatible_err = InitialAuthenticationError::IncompatibleProtocol { + client: client_protocol_version, + current: CURRENT_PROTOCOL_VERSION, + }; + + debug!("client protocol: {client_protocol_version}, ours: {CURRENT_PROTOCOL_VERSION}"); + + // gateway will reject any requests from clients that do not support auth v2 or aes256gcm + if !client_protocol_version.supports_authenticate_v2() + || !client_protocol_version.supports_aes256_gcm_siv() + { + error!("{incompatible_err}"); + return Err(incompatible_err); + } + + // we can't handle clients with higher protocol than ours + // (perhaps we could try to negotiate downgrade on our end? sounds like a nice future improvement) if client_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 - warn!("client has announced protocol version greater than one known by this gateway (v{client_protocol_version:?} vs v{}). attempting to downgrade.", GatewayProtocolVersion::CURRENT); - // we just reply with our current version, and it's up to the client to accept it or terminate the connection - Some(GatewayProtocolVersion::CURRENT) + error!("{incompatible_err}"); + Err(incompatible_err) } else { - // ##### - // On backwards compat: - // Currently it is the case that gateways will understand all previous protocol versions - // and will downgrade accordingly, but this will not always be the case. - // For example, once we remove downgrade on legacy auth, anything below version 4 will be rejected - // ##### - debug!( - "using the protocol version proposed by the client: v{client_protocol_version:?}" - ); - client_protocol_version + debug!("the client is using exactly the same (or older) protocol version as we are. We're good to continue!"); + Ok(CURRENT_PROTOCOL_VERSION) } } @@ -558,90 +530,6 @@ impl FreshHandler { } } - /// Tries to handle the received authentication request by checking correctness of the received data. - /// - /// # Arguments - /// - /// * `client_address`: address of the client wishing to authenticate. - /// * `encrypted_address`: ciphertext of the address of the client wishing to authenticate. - /// * `iv`: fresh IV received with the request. - #[instrument(skip_all - fields( - address = %address, - ) - )] - async fn handle_legacy_authenticate( - &mut self, - client_protocol_version: Option, - address: String, - enc_address: String, - raw_nonce: String, - ) -> Result - where - S: AsyncRead + AsyncWrite + Unpin, - { - debug!("handling client authentication (v1)"); - - let negotiated_protocol = self.negotiate_proposed_protocol(client_protocol_version); - // populate the negotiated protocol for future uses - self.negotiated_protocol = negotiated_protocol; - - let address = DestinationAddressBytes::try_from_base58_string(address) - .map_err(|err| InitialAuthenticationError::MalformedClientAddress(err.to_string()))?; - let encrypted_address = EncryptedAddressBytes::try_from_base58_string(enc_address)?; - let nonce = bs58::decode(&raw_nonce) - .into_vec() - .map_err(InitialAuthenticationError::MalformedIV)?; - - // validate the shared key - let Some(shared_keys) = self - .auth_v1_verify_stored_shared_key(address, encrypted_address, &nonce) - .await? - else { - // it feels weird to be returning an 'Ok' here, but I didn't want to change the existing behaviour - return Ok(InitialAuthResult::new_legacy_failed(negotiated_protocol)); - }; - - // in v1 we don't have explicit data so we have to use current timestamp - // (which does nothing but just allows us to use the same codepath) - let session_request_start = OffsetDateTime::now_utc(); - - // Check for duplicate clients - if let Some(remote_client_data) = self - .shared_state - .active_clients_store - .get_remote_client(address) - { - warn!("Detected duplicate connection for client: {address}"); - self.handle_duplicate_client(address, remote_client_data, session_request_start) - .await?; - } - - let client_id = shared_keys.client_id; - - // if applicable, push stored messages - self.push_stored_messages_to_client(address, &shared_keys.key) - .await?; - - // check the bandwidth - let bandwidth_remaining = self.authenticated_bandwidth_bytes(client_id).await?; - - Ok(InitialAuthResult::new( - Some(ClientDetails::new( - client_id, - address, - shared_keys.key, - session_request_start, - )), - ServerResponse::Authenticate { - protocol_version: negotiated_protocol, - status: true, - bandwidth_remaining, - upgrade_mode: self.upgrade_mode_enabled(), - }, - )) - } - async fn handle_authenticate_v2( &mut self, request: Box, @@ -652,9 +540,9 @@ impl FreshHandler { debug!("handling client authentication (v2)"); let negotiated_protocol = - self.negotiate_proposed_protocol(Some(request.content.protocol_version)); + self.negotiate_proposed_protocol(request.content.protocol_version)?; // populate the negotiated protocol for future uses - self.negotiated_protocol = negotiated_protocol; + self.negotiated_protocol = Some(negotiated_protocol); let address = request.content.client_identity.derive_destination_address(); @@ -733,7 +621,7 @@ impl FreshHandler { async fn register_client( &mut self, client_address: DestinationAddressBytes, - client_shared_keys: &SharedGatewayKey, + client_shared_keys: &SharedSymmetricKey, ) -> Result where S: AsyncRead + AsyncWrite + Unpin, @@ -777,7 +665,7 @@ impl FreshHandler { /// * `init_data`: init payload of the registration handshake. async fn handle_register( &mut self, - client_protocol_version: Option, + client_protocol_version: GatewayProtocolVersion, init_data: Vec, ) -> Result where @@ -821,7 +709,7 @@ impl FreshHandler { Ok(InitialAuthResult::new( Some(client_details), ServerResponse::Register { - protocol_version: self.negotiated_protocol, + protocol_version: handshake_result.negotiated_protocol, status: true, upgrade_mode, }, @@ -857,14 +745,8 @@ impl FreshHandler { { // we can handle stateless client requests without prior authentication, like `ClientControlRequest::SupportedProtocol` let auth_result = match request { - ClientControlRequest::Authenticate { - protocol_version, - address, - enc_address, - iv, - } => { - self.handle_legacy_authenticate(protocol_version, address, enc_address, iv) - .await + ClientControlRequest::Authenticate { .. } => { + return Err(InitialAuthenticationError::UnsupportedLegacyAuthentication) } ClientControlRequest::AuthenticateV2(req) => self.handle_authenticate_v2(req).await, ClientControlRequest::RegisterHandshakeInitRequest { diff --git a/gateway/src/node/client_handling/websocket/connection_handler/helpers.rs b/gateway/src/node/client_handling/websocket/connection_handler/helpers.rs index 5a5d24c5d61..11de82a382e 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/helpers.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/helpers.rs @@ -2,14 +2,14 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node::client_handling::websocket::connection_handler::fresh::InitialAuthenticationError; -use nym_gateway_requests::SharedGatewayKey; +use nym_gateway_requests::SharedSymmetricKey; use nym_gateway_storage::models::PersistedSharedKeys; use nym_sphinx::DestinationAddressBytes; use time::OffsetDateTime; pub(crate) struct KeyWithAuthTimestamp { pub(crate) client_id: i64, - pub(crate) key: SharedGatewayKey, + pub(crate) key: SharedSymmetricKey, pub(crate) last_used_authentication: Option, } @@ -21,7 +21,7 @@ impl KeyWithAuthTimestamp { let last_used_authentication = stored_shared_keys.last_used_authentication; let client_id = stored_shared_keys.client_id; - let key = SharedGatewayKey::try_from(stored_shared_keys).map_err(|source| { + let key = SharedSymmetricKey::try_from(stored_shared_keys).map_err(|source| { InitialAuthenticationError::MalformedStoredSharedKey { client_id: client.as_base58_string(), source, diff --git a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs index b8ecf12ec85..cef83e9d34d 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs @@ -3,7 +3,7 @@ use crate::config::Config; use nym_credential_verification::BandwidthFlushingBehaviourConfig; -use nym_gateway_requests::shared_key::SharedGatewayKey; +use nym_gateway_requests::shared_key::SharedSymmetricKey; use nym_gateway_requests::ServerResponse; use nym_sphinx::DestinationAddressBytes; use rand::{CryptoRng, Rng}; @@ -44,7 +44,7 @@ impl SocketStream { pub(crate) struct ClientDetails { pub(crate) address: DestinationAddressBytes, pub(crate) id: i64, - pub(crate) shared_keys: SharedGatewayKey, + pub(crate) shared_keys: SharedSymmetricKey, // note, this does **NOT ALWAYS** indicate timestamp of when client connected // it is (for v2 auth) timestamp the client **signed** when it created the request pub(crate) session_request_timestamp: OffsetDateTime, @@ -54,7 +54,7 @@ impl ClientDetails { pub(crate) fn new( id: i64, address: DestinationAddressBytes, - shared_keys: SharedGatewayKey, + shared_keys: SharedSymmetricKey, session_request_timestamp: OffsetDateTime, ) -> Self { ClientDetails { @@ -78,20 +78,6 @@ impl InitialAuthResult { server_response, } } - - fn new_legacy_failed(protocol_version: Option) -> Self { - InitialAuthResult { - client_details: None, - server_response: ServerResponse::Authenticate { - protocol_version, - status: false, - bandwidth_remaining: 0, - // given this response is given only to legacy clients, - // we use the default value as clients wouldn't deserialise it anyway - upgrade_mode: false, - }, - } - } } // imo there's no point in including the peer address in anything higher than debug diff --git a/nym-api/src/network_monitor/monitor/sender.rs b/nym-api/src/network_monitor/monitor/sender.rs index 358fb8e3167..1e87c9e433a 100644 --- a/nym-api/src/network_monitor/monitor/sender.rs +++ b/nym-api/src/network_monitor/monitor/sender.rs @@ -14,10 +14,10 @@ use nym_bandwidth_controller::BandwidthController; use nym_credential_storage::persistent_storage::PersistentStorage; use nym_crypto::asymmetric::ed25519; use nym_gateway_client::client::config::GatewayClientConfig; -use nym_gateway_client::client::GatewayConfig; +use nym_gateway_client::client::{GatewayConfig, GatewayListeners}; use nym_gateway_client::error::GatewayClientError; use nym_gateway_client::{ - AcknowledgementReceiver, GatewayClient, MixnetMessageReceiver, PacketRouter, SharedGatewayKey, + AcknowledgementReceiver, GatewayClient, MixnetMessageReceiver, PacketRouter, SharedSymmetricKey, }; use nym_sphinx::forwarding::packet::MixPacket; use nym_task::ShutdownToken; @@ -30,6 +30,7 @@ use std::sync::Arc; use std::task::Poll; use std::time::Duration; use tracing::{debug, error, info, trace, warn}; +use url::Url; const TIME_CHUNK_SIZE: Duration = Duration::from_millis(50); @@ -58,14 +59,17 @@ impl GatewayPackets { } } - pub(crate) fn gateway_config(&self) -> Option { - self.clients_address - .clone() - .map(|gateway_listener| GatewayConfig { + pub(crate) fn gateway_config(&self) -> Result, url::ParseError> { + match self.clients_address.as_ref() { + Some(gateway_listener) => Ok(Some(GatewayConfig { gateway_identity: self.pub_key, - gateway_owner: None, - gateway_listener, - }) + gateway_listeners: GatewayListeners { + primary: Url::parse(gateway_listener)?, + fallback: None, + }, + })), + None => Ok(None), + } } pub(crate) fn empty(clients_address: Option, pub_key: ed25519::PublicKey) -> Self { @@ -96,7 +100,7 @@ struct FreshGatewayClientData { gateway_response_timeout: Duration, bandwidth_controller: BandwidthController, disabled_credentials_mode: bool, - gateways_key_cache: DashMap>, + gateways_key_cache: DashMap>, } impl FreshGatewayClientData { @@ -267,7 +271,7 @@ impl PacketSender { connection_timeout: Duration, bandwidth_claim_timeout: Duration, client: &mut GatewayClientHandle, - ) -> Option> { + ) -> Option> { let gateway_identity = client.gateway_identity(); // 1. attempt to authenticate @@ -365,9 +369,16 @@ impl PacketSender { ) -> Option { let identity = packets.pub_key; - let Some(gateway_config) = packets.gateway_config() else { - warn!("gateway {identity} didn't provide valid entry information"); - return None; + let gateway_config = match packets.gateway_config() { + Ok(Some(gateway_config)) => gateway_config, + Ok(None) => { + warn!("gateway {identity} didn't provide valid entry information"); + return None; + } + Err(e) => { + warn!("Error while parsing entry information for gateway {identity} : {e}"); + return None; + } }; let (mut client, gateway_channels) = diff --git a/nym-registration-client/src/builder/config.rs b/nym-registration-client/src/builder/config.rs index f5fac943618..d566c397f0f 100644 --- a/nym-registration-client/src/builder/config.rs +++ b/nym-registration-client/src/builder/config.rs @@ -119,6 +119,7 @@ impl BuilderConfig { .network_details(self.network_env) .debug_config(debug_config) .credentials_mode(true) + .no_hostname(true) .with_remember_me(remember_me) .custom_topology_provider(self.custom_topology_provider); diff --git a/sdk/rust/nym-sdk/examples/manually_handle_storage.rs b/sdk/rust/nym-sdk/examples/manually_handle_storage.rs index 87ba92c2ec8..5ae2bbed8d3 100644 --- a/sdk/rust/nym-sdk/examples/manually_handle_storage.rs +++ b/sdk/rust/nym-sdk/examples/manually_handle_storage.rs @@ -1,5 +1,4 @@ -use nym_crypto::asymmetric::ed25519::PublicKey; -use nym_gateway_requests::SharedSymmetricKey; +use nym_client_core::client::base_client::storage::gateways_storage::GatewayPublishedData; use nym_sdk::mixnet::{ self, ActiveGateway, BadGateway, ClientKeys, EmptyReplyStorage, EphemeralCredentialStorage, GatewayRegistration, GatewaysDetailsStore, KeyStore, MixnetClientStorage, MixnetMessageSender, @@ -166,14 +165,14 @@ impl GatewaysDetailsStore for MockGatewayDetailsStore { Ok(()) } - async fn upgrade_stored_remote_gateway_key( + async fn update_gateway_published_data( &self, - gateway_id: PublicKey, - _updated_key: &SharedSymmetricKey, + _gateway_id: &str, + _details: &GatewayPublishedData, ) -> Result<(), Self::StorageError> { - println!("upgrading gateway key for {gateway_id}"); + println!("updating gateway details"); - Err(MyError) + Ok(()) } async fn remove_gateway_details(&self, _gateway_id: &str) -> Result<(), Self::StorageError> { diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index 27c9983f7fd..5020c1bd5c8 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -9,6 +9,7 @@ use crate::GatewayTransceiver; use crate::NymNetworkDetails; use crate::{Error, Result}; use log::{debug, warn}; +use nym_client_core::client::base_client::storage::gateways_storage::GatewayRegistration; use nym_client_core::client::base_client::storage::helpers::{ get_active_gateway_identity, get_all_registered_identities, has_gateway_details, set_active_gateway, @@ -24,8 +25,8 @@ use nym_client_core::client::{ use nym_client_core::config::{DebugConfig, ForgetMe, RememberMe, StatsReporting}; use nym_client_core::error::ClientCoreError; use nym_client_core::init::helpers::gateways_for_init; -use nym_client_core::init::setup_gateway; use nym_client_core::init::types::{GatewaySelectionSpecification, GatewaySetup}; +use nym_client_core::init::{refresh_gateway_published_data, setup_gateway}; use nym_credentials_interface::TicketType; use nym_crypto::hkdf::DerivationMaterial; use nym_socks5_client_core::config::Socks5; @@ -56,6 +57,7 @@ pub struct MixnetClientBuilder { custom_shutdown: Option, event_tx: Option, force_tls: bool, + no_hostname: bool, user_agent: Option, #[cfg(unix)] connection_fd_callback: Option>, @@ -101,6 +103,7 @@ impl MixnetClientBuilder { event_tx: None, custom_gateway_transceiver: None, force_tls: false, + no_hostname: false, user_agent: None, #[cfg(unix)] connection_fd_callback: None, @@ -134,6 +137,7 @@ where custom_shutdown: None, event_tx: None, force_tls: false, + no_hostname: false, user_agent: None, #[cfg(unix)] connection_fd_callback: None, @@ -158,6 +162,7 @@ where custom_shutdown: self.custom_shutdown, event_tx: self.event_tx, force_tls: self.force_tls, + no_hostname: self.no_hostname, user_agent: self.user_agent, #[cfg(unix)] connection_fd_callback: self.connection_fd_callback, @@ -229,6 +234,13 @@ where self } + /// Attempt to only choose a gateway with its IP address only, ignored if force_tls is set + #[must_use] + pub fn no_hostname(mut self, no_hostname: bool) -> Self { + self.no_hostname = no_hostname; + self + } + /// Enable paid coconut bandwidth credentials mode. #[must_use] pub fn enable_credentials_mode(mut self) -> Self { @@ -341,6 +353,7 @@ where client.custom_shutdown = self.custom_shutdown; client.wait_for_gateway = self.wait_for_gateway; client.force_tls = self.force_tls; + client.no_hostname = self.no_hostname; client.user_agent = self.user_agent; #[cfg(unix)] if self.connection_fd_callback.is_some() { @@ -393,6 +406,9 @@ where /// Force the client to connect using wss protocol with the gateway. force_tls: bool, + /// Force the client to pick gateway IP and not hostname, ignored if force_tls is set + no_hostname: bool, + /// Allows passing an externally controlled shutdown handle. custom_shutdown: Option, @@ -461,6 +477,7 @@ where custom_gateway_transceiver: None, wait_for_gateway: false, force_tls: false, + no_hostname: false, custom_shutdown: None, event_tx, user_agent: None, @@ -580,6 +597,7 @@ where self.config.user_chosen_gateway.clone(), None, self.force_tls, + self.no_hostname, ); let available_gateways = self.available_gateways().await?; @@ -592,6 +610,21 @@ where }) } + async fn refresh_gateway_published_data( + &mut self, + gateway_registration: GatewayRegistration, + ) -> Result<(), ClientCoreError> { + let available_gateways = self.available_gateways().await?; + refresh_gateway_published_data( + self.storage.gateway_details_store(), + gateway_registration, + available_gateways, + self.force_tls, + self.no_hostname, + ) + .await + } + /// Register with a gateway. If a gateway is provided in the config then that will try to be /// used. If none is specified, a gateway at random will be picked. The used gateway is saved /// as the active gateway. @@ -647,6 +680,12 @@ where ) .await?; + // update gateway setup if needed + if init_results.exipred_details() { + self.refresh_gateway_published_data(init_results.gateway_registration.clone()) + .await?; + } + set_active_gateway( self.storage.gateway_details_store(), &init_results.gateway_id().to_base58_string(), diff --git a/wasm/node-tester/src/tester.rs b/wasm/node-tester/src/tester.rs index 9a48951ea7b..599e9ce3f99 100644 --- a/wasm/node-tester/src/tester.rs +++ b/wasm/node-tester/src/tester.rs @@ -26,7 +26,6 @@ use tsify::Tsify; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::future_to_promise; use wasm_client_core::client::base_client::storage::gateways_storage::GatewayDetails; -use wasm_client_core::client::base_client::storage::GatewaysDetailsStore; use wasm_client_core::client::mix_traffic::transceiver::PacketRouter; use wasm_client_core::helpers::{ current_network_topology_async, setup_from_topology, EphemeralCredentialStorage, @@ -200,8 +199,7 @@ impl NymNodeTesterBuilder { } else { let cfg = GatewayConfig::new( gateway_info.gateway_id, - gateway_info.gateway_owner_address.map(|a| a.to_string()), - gateway_info.gateway_listener.to_string(), + gateway_info.published_data.listeners, ); GatewayClient::new( GatewayClientConfig::new_default().with_disabled_credentials_mode(true), @@ -215,13 +213,8 @@ impl NymNodeTesterBuilder { ) }; - let auth_res = gateway_client.perform_initial_authentication().await?; - if auth_res.requires_key_upgrade { - let updated_key = gateway_client.upgrade_key_authenticated().await?; - client_store - .upgrade_stored_remote_gateway_key(gateway_identity, &updated_key) - .await?; - } + let _auth_res = gateway_client.perform_initial_authentication().await?; + gateway_client.claim_initial_bandwidth().await?; gateway_client.start_listening_for_mixnet_messages()?;