From 7cb2fbeba5008cb894d0473d4c0d0672d2df0038 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Fri, 28 Nov 2025 10:17:43 +0100 Subject: [PATCH 1/9] one commit to rule them all --- Cargo.lock | 2 +- ...b19f15d22fff72490473587a14cdc046fda32.json | 4 +- ...6571f6e920eb2290f20b1b8c5b0ab4b489985.json | 44 ---- ...3a7a93afb7e4d2e54e2d38fd79d31c8575a54.json | 12 - ...8edcdf149acfd38c56a4db3bbf97bdb13210.json} | 4 +- ...eabb05f0e3ddbec04fdfa111d0fc86ba75505.json | 38 +++ ...39a8ff8a26400c1d2bd45a689970bf1ba0e66.json | 12 + ...b60ccb76d6468f359891428c0bfb96ddd7ef.json} | 4 +- ...ebc7d4f895fc4991da18ec02c9e250bea0fe0.json | 12 + ...597b4f51092e30a41c4085a8f8668f039f7c0.json | 12 - .../client-core/gateways-storage/Cargo.toml | 2 +- ...51126120000_remove_legacy_add_fallback.sql | 26 ++ .../src/backend/fs_backend/manager.rs | 43 ++- .../src/backend/fs_backend/mod.rs | 17 -- .../src/backend/fs_backend/models.rs | 2 - .../src/backend/mem_backend.rs | 27 +- .../client-core/gateways-storage/src/error.rs | 10 - .../client-core/gateways-storage/src/lib.rs | 7 - .../client-core/gateways-storage/src/types.rs | 108 +++----- .../src/cli_helpers/client_add_gateway.rs | 3 +- .../src/cli_helpers/client_list_gateways.rs | 4 +- common/client-core/src/cli_helpers/types.rs | 4 + .../client-core/src/client/base_client/mod.rs | 36 +-- .../base_client/storage/migration_helpers.rs | 204 +------------- .../client-core/src/client/key_manager/mod.rs | 4 +- common/client-core/src/error.rs | 3 + common/client-core/src/init/helpers.rs | 13 +- common/client-core/src/init/mod.rs | 13 +- common/client-core/src/init/types.rs | 35 ++- .../gateway-client/src/client/mod.rs | 248 ++++++------------ .../gateway-client/src/client/websockets.rs | 42 ++- .../client-libs/gateway-client/src/error.rs | 5 +- common/client-libs/gateway-client/src/lib.rs | 6 +- .../gateway-client/src/socket_state.rs | 8 +- .../src/authentication/encrypted_address.rs | 73 ------ .../src/authentication/mod.rs | 4 - common/gateway-requests/src/lib.rs | 9 +- .../src/registration/handshake/client.rs | 15 +- .../src/registration/handshake/gateway.rs | 5 +- .../src/registration/handshake/messages.rs | 73 ++---- .../src/registration/handshake/mod.rs | 60 +---- .../src/registration/handshake/state.rs | 93 +++---- .../src/shared_key/helpers.rs | 98 ------- .../gateway-requests/src/shared_key/legacy.rs | 246 ----------------- common/gateway-requests/src/shared_key/mod.rs | 201 ++------------ .../src/types/binary_request.rs | 8 +- .../src/types/binary_response.rs | 8 +- common/gateway-requests/src/types/helpers.rs | 45 ++-- .../types/registration_handshake_wrapper.rs | 26 +- .../src/types/text_request/authenticate.rs | 17 +- .../src/types/text_request/mod.rs | 126 ++++----- .../src/types/text_response.rs | 26 +- .../20251126120000_remove_aes128ctr_key.sql | 22 ++ common/gateway-storage/src/lib.rs | 7 +- common/gateway-storage/src/models.rs | 27 +- common/gateway-storage/src/shared_keys.rs | 8 +- common/gateway-storage/src/traits.rs | 4 +- common/topology/src/node.rs | 11 + .../src/storage/core_client_traits.rs | 15 -- common/wasm/client-core/src/storage/types.rs | 25 +- .../src/storage/wasm_client_traits.rs | 18 -- .../connection_handler/authenticated.rs | 41 +-- .../websocket/connection_handler/fresh.rs | 205 ++++----------- .../websocket/connection_handler/helpers.rs | 6 +- .../websocket/connection_handler/mod.rs | 20 +- nym-api/src/network_monitor/monitor/sender.rs | 39 ++- .../examples/manually_handle_storage.rs | 12 - wasm/node-tester/src/tester.rs | 59 ++--- 68 files changed, 690 insertions(+), 1976 deletions(-) delete mode 100644 common/client-core/gateways-storage/.sqlx/query-0e85ec18da67cf4e3df04ad80136571f6e920eb2290f20b1b8c5b0ab4b489985.json delete mode 100644 common/client-core/gateways-storage/.sqlx/query-0f1dfb89f1eb39f4a58787af0f53a7a93afb7e4d2e54e2d38fd79d31c8575a54.json rename common/client-core/gateways-storage/.sqlx/{query-b059bc3688b6b7f83f47048db9897720fd4e6f3211bf74030a9638f7bf6738e4.json => query-2c113b37864f9fec7e64c0f8fdd38edcdf149acfd38c56a4db3bbf97bdb13210.json} (56%) create mode 100644 common/client-core/gateways-storage/.sqlx/query-4b739e12ea8d917cb17580337caeabb05f0e3ddbec04fdfa111d0fc86ba75505.json create mode 100644 common/client-core/gateways-storage/.sqlx/query-700a75acbcd90c74baa7823c40739a8ff8a26400c1d2bd45a689970bf1ba0e66.json rename common/client-core/gateways-storage/.sqlx/{query-8909fd329e7e5fb16c4989b15b3d3a12bba1569520e01f6f074178e23d6ee89e.json => query-727598e516090da6d26e36d09062b60ccb76d6468f359891428c0bfb96ddd7ef.json} (51%) create mode 100644 common/client-core/gateways-storage/.sqlx/query-a64a557ba87d4b2c7457857afa7ebc7d4f895fc4991da18ec02c9e250bea0fe0.json delete mode 100644 common/client-core/gateways-storage/.sqlx/query-a6939bea03b10cde810a9a099bd597b4f51092e30a41c4085a8f8668f039f7c0.json create mode 100644 common/client-core/gateways-storage/fs_gateways_migrations/20251126120000_remove_legacy_add_fallback.sql delete mode 100644 common/client-core/gateways-storage/src/backend/fs_backend/models.rs delete mode 100644 common/gateway-requests/src/authentication/encrypted_address.rs delete mode 100644 common/gateway-requests/src/authentication/mod.rs delete mode 100644 common/gateway-requests/src/shared_key/helpers.rs delete mode 100644 common/gateway-requests/src/shared_key/legacy.rs create mode 100644 common/gateway-storage/migrations/20251126120000_remove_aes128ctr_key.sql 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..a1db63ca0da --- /dev/null +++ b/common/client-core/gateways-storage/fs_gateways_migrations/20251126120000_remove_legacy_add_fallback.sql @@ -0,0 +1,26 @@ +/* + * 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), + gateway_listener TEXT NOT NULL, + fallback_listener TEXT, + expiration_timestamp TIMESTAMP WITHOUT TIME ZONE +); + +CREATE TABLE remote_gateway_shared_keys +( + gateway_id_bs58 TEXT NOT NULL UNIQUE PRIMARY KEY REFERENCES registered_gateway (gateway_id_bs58), + derived_aes256_gcm_siv_key BLOB NOT NULL +); + +INSERT INTO remote_gateway_shared_keys SELECT gateway_id_bs58, derived_aes256_gcm_siv_key FROM remote_gateway_details; + +DROP TABLE remote_gateway_details; +ALTER TABLE remote_gateway_details_temp RENAME TO remote_gateway_details; + + + 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..f2f645f2df3 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 @@ -146,7 +146,19 @@ impl StorageManager { ) -> Result { sqlx::query_as!( RawRemoteGatewayDetails, - "SELECT * FROM remote_gateway_details WHERE gateway_id_bs58 = ?", + "SELECT + rgd.gateway_id_bs58, + derived_aes256_gcm_siv_key, + gateway_listener, + fallback_listener + FROM + remote_gateway_details AS rgd + INNER JOIN + remote_gateway_shared_keys AS rgsk + ON + rgd.gateway_id_bs58 = rgsk.gateway_id_bs58 + WHERE + rgd.gateway_id_bs58 = ?", gateway_id ) .fetch_one(&self.connection_pool) @@ -159,41 +171,26 @@ 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) - VALUES (?, ?, ?, ?, ?) + INSERT INTO remote_gateway_details(gateway_id_bs58, gateway_listener, fallback_listener) + 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.fallback_listener ) .execute(&self.connection_pool) .await?; - Ok(()) - } - pub(crate) 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<(), 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 = ? + INSERT INTO remote_gateway_shared_keys(gateway_id_bs58, derived_aes256_gcm_siv_key) + VALUES (?, ?) "#, - derived_aes128_ctr_blake3_hmac_keys_bs58, - derived_aes256_gcm_siv_key, - gateway_id_bs58 + remote.gateway_id_bs58, + remote.derived_aes256_gcm_siv_key ) .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..cb8abe3cebf 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 @@ -8,12 +8,10 @@ use crate::{ 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,21 +132,6 @@ impl GatewaysDetailsStore for OnDiskGatewaysDetails { Ok(()) } - async fn upgrade_stored_remote_gateway_key( - &self, - gateway_id: ed25519::PublicKey, - updated_key: &SharedSymmetricKey, - ) -> Result<(), Self::StorageError> { - self.manager - .update_remote_gateway_key( - &gateway_id.to_base58_string(), - None, - Some(updated_key.as_bytes()), - ) - .await?; - Ok(()) - } - // ideally all of those should be run under a storage tx to ensure storage consistency, // but at that point it's fine async fn remove_gateway_details(&self, gateway_id: &str) -> Result<(), Self::StorageError> { 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..d099eee0941 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, 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,29 +94,6 @@ impl GatewaysDetailsStore for InMemGatewaysDetails { Ok(()) } - async fn upgrade_stored_remote_gateway_key( - &self, - gateway_id: PublicKey, - updated_key: &SharedSymmetricKey, - ) -> 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(), - )) - } - - Ok(()) - } - async fn remove_gateway_details(&self, gateway_id: &str) -> Result<(), Self::StorageError> { let mut guard = self.inner.write().await; if let Some(active) = guard.active_gateway.as_ref() { diff --git a/common/client-core/gateways-storage/src/error.rs b/common/client-core/gateways-storage/src/error.rs index 2c251f513db..bf87be94b28 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, diff --git a/common/client-core/gateways-storage/src/lib.rs b/common/client-core/gateways-storage/src/lib.rs index fd3f1af1e36..c88a909298f 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,12 +59,6 @@ pub trait GatewaysDetailsStore { details: &GatewayRegistration, ) -> Result<(), Self::StorageError>; - async fn upgrade_stored_remote_gateway_key( - &self, - gateway_id: ed25519::PublicKey, - updated_key: &SharedSymmetricKey, - ) -> Result<(), Self::StorageError>; - /// Remove given gateway details from the underlying store. async fn remove_gateway_details(&self, gateway_id: &str) -> Result<(), Self::StorageError>; } diff --git a/common/client-core/gateways-storage/src/types.rs b/common/client-core/gateways-storage/src/types.rs index 477e9df99f3..f8a13b20f2b 100644 --- a/common/client-core/gateways-storage/src/types.rs +++ b/common/client-core/gateways-storage/src/types.rs @@ -2,12 +2,11 @@ // 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::OffsetDateTime; @@ -65,15 +64,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, + gateway_listeners: GatewayListeners, ) -> Self { GatewayDetails::Remote(RemoteGatewayDetails { gateway_id, shared_key, - gateway_owner_address, - gateway_listener, + gateway_listeners, }) } @@ -88,7 +85,7 @@ 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, @@ -168,10 +165,9 @@ pub struct RegisteredGateway { #[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 derived_aes256_gcm_siv_key: Vec, pub gateway_listener: String, + pub fallback_listener: Option, } impl TryFrom for RemoteGatewayDetails { @@ -186,49 +182,11 @@ 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 shared_key = SharedSymmetricKey::try_from_bytes(&value.derived_aes256_gcm_siv_key) + .map_err(|source| BadGateway::MalformedSharedKeys { + gateway_id: value.gateway_id_bs58.clone(), + source, + })?; let gateway_listener = Url::parse(&value.gateway_listener).map_err(|source| { BadGateway::MalformedListener { @@ -237,30 +195,40 @@ impl TryFrom for RemoteGatewayDetails { source, } })?; + let fallback_listener = value + .fallback_listener + .as_ref() + .map(|uri| { + Url::parse(uri).map_err(|source| BadGateway::MalformedListener { + gateway_id: value.gateway_id_bs58.clone(), + raw_listener: uri.to_owned(), + source, + }) + }) + .transpose()?; Ok(RemoteGatewayDetails { gateway_id, shared_key: Arc::new(shared_key), - gateway_owner_address, - gateway_listener, + gateway_listeners: GatewayListeners { + primary: gateway_listener, + fallback: fallback_listener, + }, }) } } 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(), + gateway_listener: value.gateway_listeners.primary.to_string(), + fallback_listener: value + .gateway_listeners + .fallback + .as_ref() + .map(|uri| uri.to_string()), } } } @@ -269,11 +237,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 gateway_listeners: GatewayListeners, } #[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..ed0c2a8ccb1 100644 --- a/common/client-core/src/cli_helpers/client_add_gateway.rs +++ b/common/client-core/src/cli_helpers/client_add_gateway.rs @@ -167,6 +167,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.gateway_listeners.primary.clone()), + fallback_endpoint: gateway_details.gateway_listeners.fallback.clone(), }) } 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..126397aa177 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.gateway_listeners.primary), + fallback_endpoint: remote_details.gateway_listeners.fallback, }), 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..63cfc4ac53b 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.gateway_listeners); GatewayClient::new( GatewayClientConfig::new_default() .with_disabled_credentials_mode(config.client.disabled_credentials_mode) @@ -593,31 +585,13 @@ where // 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 _auth_res = 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 +610,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 +640,6 @@ where config, initialisation_result, bandwidth_controller, - details_store, packet_router, stats_reporter, #[cfg(unix)] @@ -975,8 +947,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 +1040,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/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..0508dd9431f 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), 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..7cc5f8e963f 100644 --- a/common/client-core/src/init/mod.rs +++ b/common/client-core/src/init/mod.rs @@ -105,27 +105,22 @@ 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, ) .await?; ( - GatewayDetails::new_remote( - gateway_id, - registration.shared_keys, - gateway_owner_address, - gateway_listener, - ), + GatewayDetails::new_remote(gateway_id, registration.shared_keys, gateway_listeners), Some(registration.authenticated_ephemeral_client), ) } diff --git a/common/client-core/src/init/types.rs b/common/client-core/src/init/types.rs index 8f09980a43e..ab0264d356d 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, @@ -59,6 +56,16 @@ impl SelectedGateway { })? }; + let fallback_listener = + node.ws_entry_address_no_hostname(prefer_ipv6) + .and_then(|address| { + Url::parse(&address) + .inspect_err(|err| { + tracing::warn!("Malformed fallback listener, none will be used : {err}") + }) + .ok() + }); + let gateway_listener = Url::parse(&gateway_listener).map_err(|source| ClientCoreError::MalformedListener { gateway_id: node.identity_key.to_base58_string(), @@ -68,8 +75,10 @@ impl SelectedGateway { Ok(SelectedGateway::Remote { gateway_id: node.identity_key, - gateway_owner_address: None, - gateway_listener, + gateway_listeners: GatewayListeners { + primary: gateway_listener, + fallback: fallback_listener, + }, }) } @@ -98,7 +107,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, } @@ -315,6 +324,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 +342,12 @@ 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.gateway_listeners.primary.to_string(), + fallback_listener: gateway + .gateway_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..5918457d052 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() } @@ -205,10 +201,10 @@ impl GatewayClient { pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> { debug!( "Attempting to establish connection to gateway at: {}", - self.gateway_address + self.gateway_addresses.primary ); - let (ws_stream, _) = connect_async( - &self.gateway_address, + let (ws_stream, _) = connect_async_with_fallback( + &self.gateway_addresses, #[cfg(unix)] self.connection_fd_callback.clone(), ) @@ -221,7 +217,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 +270,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 { @@ -461,21 +457,46 @@ impl GatewayClient { } } + fn check_gateway_protocol( + &self, + gateway_protocol: GatewayProtocolVersion, + ) -> Result<(), GatewayClientError> { + debug!("gateway protocol: {gateway_protocol:?}, ours: {CURRENT_PROTOCOL_VERSION}"); + + // client should reject any gateways that do not indicate they support auth v2 or aes256gcm-siv + if !gateway_protocol.supports_authenticate_v2() + || !gateway_protocol.supports_aes256_gcm_siv() + { + return Err(GatewayClientError::IncompatibleProtocol { + gateway: gateway_protocol, + current: CURRENT_PROTOCOL_VERSION, + }); + } + + // we can't handle gateways with higher protocol than ours + if gateway_protocol.is_future_version() { + let err = GatewayClientError::IncompatibleProtocol { + gateway: gateway_protocol, + current: CURRENT_PROTOCOL_VERSION, + }; + error!("{err}"); + Err(err) + } else { + debug!("the gateway is using exactly the same (or older) protocol version as we are. We're good to continue!"); + Ok(()) + } + } + async fn register( &mut self, - 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 @@ -513,6 +534,7 @@ impl GatewayClient { other => return Err(GatewayClientError::UnexpectedResponse { name: other.name() }), }; + self.check_gateway_protocol(handshake_result.negotiated_protocol)?; self.authenticated = authentication_status; if self.authenticated { @@ -525,75 +547,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, @@ -605,18 +558,12 @@ impl GatewayClient { bandwidth_remaining, 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"); - return Err(GatewayClientError::AuthenticationFailure); - } + self.check_gateway_protocol(protocol_version)?; 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 +576,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 +596,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 +622,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 +639,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 +658,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 +668,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 +682,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 +699,6 @@ impl GatewayClient { // so no upgrades are required Ok(AuthenticationResponse { initial_shared_key: Arc::clone(shared_key), - requires_key_upgrade: false, }) } } @@ -1143,7 +1060,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 +1110,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 +1129,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 +1161,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..474f2a216b7 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; @@ -11,7 +13,9 @@ use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; use tungstenite::handshake::client::Response; use url::{Host, Url}; -use std::net::SocketAddr; +use std::{net::SocketAddr, time::Duration}; + +const INITIAL_CONNECTION_TIMEOUT: Duration = Duration::from_secs(5); #[cfg(not(target_arch = "wasm32"))] pub(crate) async fn connect_async( @@ -85,3 +89,39 @@ 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> { + // Hickory DNS has a non cofigurable 2 * 10 seconds timeout so we have to add one here as well + match tokio::time::timeout( + INITIAL_CONNECTION_TIMEOUT, + connect_async( + endpoints.primary.as_ref(), + #[cfg(unix)] + connection_fd_callback.clone(), + ), + ) + .await + { + Ok(inner) => inner, + Err(_) if endpoints.fallback.is_some() => { + // SAFTEY: We know there is a fallback here + #[allow(clippy::unwrap_used)] + let fallback = endpoints.fallback.as_ref().unwrap().to_string(); + tracing::warn!( + "Timeout trying to connect to endpoint {}, trying fallback : {fallback}", + endpoints.primary + ); + connect_async( + &fallback, + #[cfg(unix)] + connection_fd_callback, + ) + .await + } + Err(_) => Err(GatewayClientError::Timeout), + } +} diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index 9f97b7c7f58..532296eab23 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), @@ -117,7 +120,7 @@ pub enum GatewayClientError { 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 }, + 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..a82e2067b72 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<()>, @@ -294,7 +294,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..b7a2264336f 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()); + 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..d5f6868b871 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 { @@ -65,17 +65,12 @@ impl HandshakeMessage for Initialisation { // 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..a8b91513854 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 // 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/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 index c0a72135a3f..912b802cf41 100644 --- a/common/gateway-requests/src/shared_key/mod.rs +++ b/common/gateway-requests/src/shared_key/mod.rs @@ -7,94 +7,15 @@ 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 nym_sphinx::params::GatewayEncryptionAlgorithm; 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")] @@ -113,109 +34,6 @@ pub enum SharedKeyUsageError { 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); @@ -232,6 +50,23 @@ pub enum SharedKeyConversionError { } 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 { 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..c915598523f 100644 --- a/common/gateway-requests/src/types/helpers.rs +++ b/common/gateway-requests/src/types/helpers.rs @@ -3,7 +3,7 @@ use crate::{ BinaryRequest, BinaryRequestKind, BinaryResponse, BinaryResponseKind, GatewayRequestsError, - SharedGatewayKey, + SharedSymmetricKey, }; use std::iter::once; @@ -22,11 +22,7 @@ pub struct BinaryData<'a> { 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(); - } - + pub fn into_raw(self) -> 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()) @@ -40,19 +36,8 @@ impl<'a> BinaryData<'a> { // 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); } @@ -83,30 +68,33 @@ 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(), + maybe_nonce: Some(&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 raw_nonce = self.maybe_nonce.unwrap_or(&[]); + let nonce = SharedSymmetricKey::validate_aead_nonce(raw_nonce)?; + + &*key.decrypt(self.data, &nonce)? } else { self.data }; @@ -117,13 +105,16 @@ 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 raw_nonce = self.maybe_nonce.unwrap_or(&[]); + let nonce = SharedSymmetricKey::validate_aead_nonce(raw_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..ffbc2982588 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,18 @@ 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, + #[serde(alias = "iv")] + nonce: Vec, }, UpgradeModeJWT { // no need to encrypt it as it's public anyway @@ -109,40 +83,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 +137,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..1792ee5a1cb 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; @@ -18,9 +18,9 @@ pub enum SensitiveServerResponse { } 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 +30,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 +76,7 @@ pub struct SendResponse { pub enum ServerResponse { Authenticate { #[serde(default)] - protocol_version: Option, + protocol_version: GatewayProtocolVersion, status: bool, bandwidth_remaining: i64, @@ -83,7 +87,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..a1917086839 --- /dev/null +++ b/common/gateway-storage/migrations/20251126120000_remove_aes128ctr_key.sql @@ -0,0 +1,22 @@ +/* + * 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 +); + +INSERT INTO shared_keys_tmp (client_id, client_address_bs58, derived_aes256_gcm_siv_key) +SELECT client_id, client_address_bs58, derived_aes256_gcm_siv_key +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..29bf5bf7d58 100644 --- a/common/topology/src/node.rs +++ b/common/topology/src/node.rs @@ -89,6 +89,17 @@ impl RoutingNode { self.ws_entry_address_no_tls(prefer_ipv6) } + pub fn ws_entry_address_no_hostname(&self, prefer_ipv6: bool) -> Option { + let entry = self.entry.as_ref()?; + + if prefer_ipv6 && let Some(ipv6) = entry.ip_addresses.iter().find(|ip| ip.is_ipv6()) { + return Some(format!("ws://{ipv6}:{}", entry.clients_ws_port)); + } + + let any_ip = entry.ip_addresses.first()?; + Some(format!("ws://{any_ip}:{}", entry.clients_ws_port)) + } + pub fn identity(&self) -> ed25519::PublicKey { self.identity_key } 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..928736e8728 100644 --- a/common/wasm/client-core/src/storage/core_client_traits.rs +++ b/common/wasm/client-core/src/storage/core_client_traits.rs @@ -15,8 +15,6 @@ 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,19 +154,6 @@ impl GatewaysDetailsStore for ClientStorage { self.store_registered_gateway(&raw_registration).await } - async fn upgrade_stored_remote_gateway_key( - &self, - gateway_id: PublicKey, - updated_key: &SharedSymmetricKey, - ) -> Result<(), Self::StorageError> { - self.update_remote_gateway_key( - &gateway_id.to_base58_string(), - None, - Some(updated_key.as_bytes()), - ) - .await - } - async fn remove_gateway_details(&self, gateway_id: &str) -> Result<(), Self::StorageError> { self.remove_registered_gateway(gateway_id).await } diff --git a/common/wasm/client-core/src/storage/types.rs b/common/wasm/client-core/src/storage/types.rs index b94f04f6514..b6e11919feb 100644 --- a/common/wasm/client-core/src/storage/types.rs +++ b/common/wasm/client-core/src/storage/types.rs @@ -4,9 +4,7 @@ use nym_client_core::client::base_client::storage::gateways_storage::{ BadGateway, GatewayDetails, GatewayRegistration, RawRemoteGatewayDetails, RemoteGatewayDetails, }; -use nym_gateway_client::SharedGatewayKey; use serde::{Deserialize, Serialize}; -use std::ops::Deref; use time::OffsetDateTime; use zeroize::Zeroize; @@ -18,12 +16,8 @@ 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, } @@ -35,11 +29,9 @@ 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, + fallback_listener: None, }; let remote: RemoteGatewayDetails = raw_remote.try_into()?; @@ -56,22 +48,13 @@ 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()), + gateway_listener: remote_details.gateway_listeners.primary.to_string(), } } } 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..24fd9da9657 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}; @@ -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..cc226c144ca 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,13 +665,17 @@ 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 S: AsyncRead + AsyncWrite + Unpin + Send, R: CryptoRng + RngCore + Send, { + let negotiated_protocol = self.negotiate_proposed_protocol(client_protocol_version)?; + // populate the negotiated protocol for future uses + self.negotiated_protocol = Some(negotiated_protocol); + let remote_identity = Self::extract_remote_identity_from_register_init(&init_data)?; let remote_address = remote_identity.derive_destination_address(); @@ -802,9 +694,6 @@ impl FreshHandler { .await?; let shared_keys = handshake_result.derived_key; - // populate the negotiated protocol for future uses - self.negotiated_protocol = Some(handshake_result.negotiated_protocol); - let client_id = self.register_client(remote_address, &shared_keys).await?; debug!(client_id = %client_id, "managed to finalize client registration"); @@ -821,7 +710,7 @@ impl FreshHandler { Ok(InitialAuthResult::new( Some(client_details), ServerResponse::Register { - protocol_version: self.negotiated_protocol, + protocol_version: negotiated_protocol, status: true, upgrade_mode, }, @@ -857,14 +746,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/sdk/rust/nym-sdk/examples/manually_handle_storage.rs b/sdk/rust/nym-sdk/examples/manually_handle_storage.rs index 87ba92c2ec8..a6409e4106a 100644 --- a/sdk/rust/nym-sdk/examples/manually_handle_storage.rs +++ b/sdk/rust/nym-sdk/examples/manually_handle_storage.rs @@ -1,5 +1,3 @@ -use nym_crypto::asymmetric::ed25519::PublicKey; -use nym_gateway_requests::SharedSymmetricKey; use nym_sdk::mixnet::{ self, ActiveGateway, BadGateway, ClientKeys, EmptyReplyStorage, EphemeralCredentialStorage, GatewayRegistration, GatewaysDetailsStore, KeyStore, MixnetClientStorage, MixnetMessageSender, @@ -166,16 +164,6 @@ impl GatewaysDetailsStore for MockGatewayDetailsStore { Ok(()) } - async fn upgrade_stored_remote_gateway_key( - &self, - gateway_id: PublicKey, - _updated_key: &SharedSymmetricKey, - ) -> Result<(), Self::StorageError> { - println!("upgrading gateway key for {gateway_id}"); - - Err(MyError) - } - async fn remove_gateway_details(&self, _gateway_id: &str) -> Result<(), Self::StorageError> { println!("removing gateway details"); diff --git a/wasm/node-tester/src/tester.rs b/wasm/node-tester/src/tester.rs index 9a48951ea7b..d5cb5fcad87 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, @@ -189,39 +188,31 @@ impl NymNodeTesterBuilder { let stats_sender_task = task_manager.clone_shutdown_token(); - let mut gateway_client = - if let Some(existing_client) = initialisation_result.authenticated_ephemeral_client { - existing_client.upgrade( - packet_router, - self.bandwidth_controller.take(), - ClientStatsSender::new(None, stats_sender_task), - gateway_task.clone(), - ) - } 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(), - ); - GatewayClient::new( - GatewayClientConfig::new_default().with_disabled_credentials_mode(true), - cfg, - managed_keys.identity_keypair(), - Some(gateway_info.shared_key), - packet_router, - self.bandwidth_controller.take(), - ClientStatsSender::new(None, stats_sender_task), - gateway_task, - ) - }; - - 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 mut gateway_client = if let Some(existing_client) = + initialisation_result.authenticated_ephemeral_client + { + existing_client.upgrade( + packet_router, + self.bandwidth_controller.take(), + ClientStatsSender::new(None, stats_sender_task), + gateway_task.clone(), + ) + } else { + let cfg = GatewayConfig::new(gateway_info.gateway_id, gateway_info.gateway_listeners); + GatewayClient::new( + GatewayClientConfig::new_default().with_disabled_credentials_mode(true), + cfg, + managed_keys.identity_keypair(), + Some(gateway_info.shared_key), + packet_router, + self.bandwidth_controller.take(), + ClientStatsSender::new(None, stats_sender_task), + gateway_task, + ) + }; + + let _auth_res = gateway_client.perform_initial_authentication().await?; + gateway_client.claim_initial_bandwidth().await?; gateway_client.start_listening_for_mixnet_messages()?; From dae8240e84d9c6e5bf304859ad791043df66da71 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Fri, 28 Nov 2025 10:17:44 +0100 Subject: [PATCH 2/9] remove too aggressive copy pasting --- .../gateway-client/src/client/mod.rs | 36 +++---------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index 5918457d052..ec8b7e58030 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -457,36 +457,6 @@ impl GatewayClient { } } - fn check_gateway_protocol( - &self, - gateway_protocol: GatewayProtocolVersion, - ) -> Result<(), GatewayClientError> { - debug!("gateway protocol: {gateway_protocol:?}, ours: {CURRENT_PROTOCOL_VERSION}"); - - // client should reject any gateways that do not indicate they support auth v2 or aes256gcm-siv - if !gateway_protocol.supports_authenticate_v2() - || !gateway_protocol.supports_aes256_gcm_siv() - { - return Err(GatewayClientError::IncompatibleProtocol { - gateway: gateway_protocol, - current: CURRENT_PROTOCOL_VERSION, - }); - } - - // we can't handle gateways with higher protocol than ours - if gateway_protocol.is_future_version() { - let err = GatewayClientError::IncompatibleProtocol { - gateway: gateway_protocol, - current: CURRENT_PROTOCOL_VERSION, - }; - error!("{err}"); - Err(err) - } else { - debug!("the gateway is using exactly the same (or older) protocol version as we are. We're good to continue!"); - Ok(()) - } - } - async fn register( &mut self, supported_gateway_protocol: GatewayProtocolVersion, @@ -534,7 +504,6 @@ impl GatewayClient { other => return Err(GatewayClientError::UnexpectedResponse { name: other.name() }), }; - self.check_gateway_protocol(handshake_result.negotiated_protocol)?; self.authenticated = authentication_status; if self.authenticated { @@ -558,7 +527,10 @@ impl GatewayClient { bandwidth_remaining, upgrade_mode, } => { - self.check_gateway_protocol(protocol_version)?; + if protocol_version.is_future_version() { + 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); From 871b150ed4478b1959f08a829f8778e309c070f6 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Fri, 28 Nov 2025 10:17:44 +0100 Subject: [PATCH 3/9] update details when outdated --- ...51126120000_remove_legacy_add_fallback.sql | 10 ++-- .../src/backend/fs_backend/manager.rs | 50 ++++++++----------- .../src/backend/fs_backend/mod.rs | 17 +++++++ .../src/backend/mem_backend.rs | 9 ++++ .../client-core/gateways-storage/src/lib.rs | 6 +++ .../client-core/gateways-storage/src/types.rs | 26 ++++++++++ .../src/client/base_client/storage/helpers.rs | 16 ++++++ common/client-core/src/error.rs | 3 ++ common/client-core/src/init/mod.rs | 38 +++++++++++++- common/client-core/src/init/types.rs | 9 ++++ .../examples/manually_handle_storage.rs | 9 ++++ sdk/rust/nym-sdk/src/mixnet/client.rs | 23 ++++++++- 12 files changed, 179 insertions(+), 37 deletions(-) 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 index a1db63ca0da..95b8abe139e 100644 --- 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 @@ -6,18 +6,14 @@ 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 TIMESTAMP WITHOUT TIME ZONE + expiration_timestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL ); -CREATE TABLE remote_gateway_shared_keys -( - gateway_id_bs58 TEXT NOT NULL UNIQUE PRIMARY KEY REFERENCES registered_gateway (gateway_id_bs58), - derived_aes256_gcm_siv_key BLOB NOT NULL -); -INSERT INTO remote_gateway_shared_keys SELECT gateway_id_bs58, derived_aes256_gcm_siv_key FROM remote_gateway_details; +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; DROP TABLE remote_gateway_details; ALTER TABLE remote_gateway_details_temp RENAME TO remote_gateway_details; 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 f2f645f2df3..ce61d7b0ad7 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 @@ -144,25 +144,10 @@ impl StorageManager { &self, gateway_id: &str, ) -> Result { - sqlx::query_as!( - RawRemoteGatewayDetails, - "SELECT - rgd.gateway_id_bs58, - derived_aes256_gcm_siv_key, - gateway_listener, - fallback_listener - FROM - remote_gateway_details AS rgd - INNER JOIN - remote_gateway_shared_keys AS rgsk - ON - rgd.gateway_id_bs58 = rgsk.gateway_id_bs58 - WHERE - rgd.gateway_id_bs58 = ?", - gateway_id - ) - .fetch_one(&self.connection_pool) - .await + 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( @@ -171,26 +156,35 @@ impl StorageManager { ) -> Result<(), sqlx::Error> { sqlx::query!( r#" - INSERT INTO remote_gateway_details(gateway_id_bs58, gateway_listener, fallback_listener) - VALUES (?, ?, ?) + 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_aes256_gcm_siv_key, remote.gateway_listener, - remote.fallback_listener + remote.fallback_listener, + remote.expiration_timestamp ) .execute(&self.connection_pool) .await?; + Ok(()) + } + pub(crate) async fn update_remote_gateway_details( + &self, + remote: &RawRemoteGatewayDetails, + ) -> Result<(), sqlx::Error> { sqlx::query!( r#" - INSERT INTO remote_gateway_shared_keys(gateway_id_bs58, derived_aes256_gcm_siv_key) - VALUES (?, ?) + UPDATE remote_gateway_details SET gateway_listener = ?, fallback_listener = ?, expiration_timestamp = ? WHERE gateway_id_bs58 = ? "#, - remote.gateway_id_bs58, - remote.derived_aes256_gcm_siv_key + remote.gateway_listener, + remote.fallback_listener, + remote.expiration_timestamp, + remote.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 cb8abe3cebf..0a158bc47a0 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 @@ -132,6 +132,23 @@ impl GatewaysDetailsStore for OnDiskGatewaysDetails { Ok(()) } + async fn update_gateway_details( + &self, + details: &GatewayRegistration, + ) -> Result<(), Self::StorageError> { + match &details.details { + GatewayDetails::Remote(remote_details) => { + let raw_details = &remote_details.into(); + self.manager + .update_remote_gateway_details(raw_details) + .await?; + } + GatewayDetails::Custom(_) => {} + } + + Ok(()) + } + // ideally all of those should be run under a storage tx to ensure storage consistency, // but at that point it's fine async fn remove_gateway_details(&self, gateway_id: &str) -> Result<(), Self::StorageError> { 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 d099eee0941..cc7fd156142 100644 --- a/common/client-core/gateways-storage/src/backend/mem_backend.rs +++ b/common/client-core/gateways-storage/src/backend/mem_backend.rs @@ -94,6 +94,15 @@ impl GatewaysDetailsStore for InMemGatewaysDetails { Ok(()) } + // It will overwrite the existing entry, which is what we ultimately want + async fn update_gateway_details( + &self, + details: &GatewayRegistration, + ) -> Result<(), Self::StorageError> { + self.store_gateway_details(details).await?; + Ok(()) + } + async fn remove_gateway_details(&self, gateway_id: &str) -> Result<(), Self::StorageError> { let mut guard = self.inner.write().await; if let Some(active) = guard.active_gateway.as_ref() { diff --git a/common/client-core/gateways-storage/src/lib.rs b/common/client-core/gateways-storage/src/lib.rs index c88a909298f..870b69f0a26 100644 --- a/common/client-core/gateways-storage/src/lib.rs +++ b/common/client-core/gateways-storage/src/lib.rs @@ -59,6 +59,12 @@ pub trait GatewaysDetailsStore { details: &GatewayRegistration, ) -> Result<(), Self::StorageError>; + /// Update the gateway details + async fn update_gateway_details( + &self, + details: &GatewayRegistration, + ) -> Result<(), Self::StorageError>; + /// Remove given gateway details from the underlying store. async fn remove_gateway_details(&self, gateway_id: &str) -> Result<(), Self::StorageError>; } diff --git a/common/client-core/gateways-storage/src/types.rs b/common/client-core/gateways-storage/src/types.rs index f8a13b20f2b..e9703d67c01 100644 --- a/common/client-core/gateways-storage/src/types.rs +++ b/common/client-core/gateways-storage/src/types.rs @@ -9,12 +9,14 @@ use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; 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 { @@ -71,6 +73,7 @@ impl GatewayDetails { gateway_id, shared_key, gateway_listeners, + expiration_timestamp: OffsetDateTime::now_utc() + GATEWAY_DETAILS_TTL, }) } @@ -92,6 +95,23 @@ impl GatewayDetails { } } + pub fn details_exipration(&self) -> Option { + match self { + GatewayDetails::Remote(details) => Some(details.expiration_timestamp), + GatewayDetails::Custom(_) => None, + } + } + + pub fn update_remote_listeners(&mut self, new_listeners: GatewayListeners) { + match self { + GatewayDetails::Remote(details) => { + details.gateway_listeners = new_listeners; + details.expiration_timestamp = OffsetDateTime::now_utc() + GATEWAY_DETAILS_TTL; + } + GatewayDetails::Custom(_) => {} + } + } + pub fn is_custom(&self) -> bool { matches!(self, GatewayDetails::Custom(..)) } @@ -168,6 +188,8 @@ pub struct RawRemoteGatewayDetails { pub derived_aes256_gcm_siv_key: Vec, pub gateway_listener: String, pub fallback_listener: Option, + #[zeroize(skip)] + pub expiration_timestamp: OffsetDateTime, } impl TryFrom for RemoteGatewayDetails { @@ -214,6 +236,7 @@ impl TryFrom for RemoteGatewayDetails { primary: gateway_listener, fallback: fallback_listener, }, + expiration_timestamp: value.expiration_timestamp, }) } } @@ -229,6 +252,7 @@ impl<'a> From<&'a RemoteGatewayDetails> for RawRemoteGatewayDetails { .fallback .as_ref() .map(|uri| uri.to_string()), + expiration_timestamp: value.expiration_timestamp, } } } @@ -240,6 +264,8 @@ pub struct RemoteGatewayDetails { pub shared_key: Arc, pub gateway_listeners: GatewayListeners, + + pub expiration_timestamp: OffsetDateTime, } #[derive(Debug, Clone, Serialize, Deserialize)] 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..ff06ed4c9dc 100644 --- a/common/client-core/src/client/base_client/storage/helpers.rs +++ b/common/client-core/src/client/base_client/storage/helpers.rs @@ -85,6 +85,22 @@ where }) } +pub async fn update_stored_gateway_details( + details_store: &D, + details: &GatewayRegistration, +) -> Result<(), ClientCoreError> +where + D: GatewaysDetailsStore, + D::StorageError: Send + Sync + 'static, +{ + details_store + .update_gateway_details(details) + .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/error.rs b/common/client-core/src/error.rs index 0508dd9431f..ce1cc12f76a 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -166,6 +166,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/mod.rs b/common/client-core/src/init/mod.rs index 7cc5f8e963f..5461950c694 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_gateway_details, }; use crate::client::key_manager::persistence::KeyStore; use crate::client::key_manager::ClientKeys; @@ -145,6 +145,42 @@ where }) } +pub async fn update_gateway_details( + details_store: &D, + mut registration: GatewayRegistration, + available_gateways: Vec, + must_use_tls: 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)?; + + 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)? + } + }; + + registration + .details + .update_remote_listeners(new_gateway_listeners); + + // update gateway details + update_stored_gateway_details(details_store, ®istration).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 ab0264d356d..e0955c0e8ee 100644 --- a/common/client-core/src/init/types.rs +++ b/common/client-core/src/init/types.rs @@ -154,6 +154,15 @@ 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)] diff --git a/sdk/rust/nym-sdk/examples/manually_handle_storage.rs b/sdk/rust/nym-sdk/examples/manually_handle_storage.rs index a6409e4106a..60544789489 100644 --- a/sdk/rust/nym-sdk/examples/manually_handle_storage.rs +++ b/sdk/rust/nym-sdk/examples/manually_handle_storage.rs @@ -164,6 +164,15 @@ impl GatewaysDetailsStore for MockGatewayDetailsStore { Ok(()) } + async fn update_gateway_details( + &self, + _details: &GatewayRegistration, + ) -> Result<(), Self::StorageError> { + println!("updating gateway details"); + + Ok(()) + } + async fn remove_gateway_details(&self, _gateway_id: &str) -> Result<(), Self::StorageError> { println!("removing gateway details"); diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index 27c9983f7fd..7b1403d0a77 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::{setup_gateway, update_gateway_details}; use nym_credentials_interface::TicketType; use nym_crypto::hkdf::DerivationMaterial; use nym_socks5_client_core::config::Socks5; @@ -592,6 +593,20 @@ where }) } + async fn update_gateway_details( + &mut self, + gateway_registration: GatewayRegistration, + ) -> Result<(), ClientCoreError> { + let available_gateways = self.available_gateways().await?; + update_gateway_details( + self.storage.gateway_details_store(), + gateway_registration, + available_gateways, + self.force_tls, + ) + .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 +662,12 @@ where ) .await?; + // update gateway setup if needed + if init_results.exipred_details() { + self.update_gateway_details(init_results.gateway_registration.clone()) + .await?; + } + set_active_gateway( self.storage.gateway_details_store(), &init_results.gateway_id().to_base58_string(), From fed65b569c286745e2164b5380f317e1eb7cdc08 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Fri, 28 Nov 2025 10:17:44 +0100 Subject: [PATCH 4/9] typo and serde alias --- common/gateway-requests/src/types/text_request/mod.rs | 4 +++- .../websocket/connection_handler/authenticated.rs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/common/gateway-requests/src/types/text_request/mod.rs b/common/gateway-requests/src/types/text_request/mod.rs index ffbc2982588..f672eefd55c 100644 --- a/common/gateway-requests/src/types/text_request/mod.rs +++ b/common/gateway-requests/src/types/text_request/mod.rs @@ -69,7 +69,9 @@ pub enum ClientControlRequest { }, EcashCredential { enc_credential: Vec, - #[serde(alias = "iv")] + // 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 { 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 24fd9da9657..189a5a4ca66 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -64,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}")] From 799d493b2d57c3fb618cf50743bc9c0b0ff0b24b Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Fri, 28 Nov 2025 10:17:44 +0100 Subject: [PATCH 5/9] no hostname option and fixes --- .../src/cli_helpers/client_add_gateway.rs | 1 + .../src/cli_helpers/client_init.rs | 1 + common/client-core/src/error.rs | 3 + common/client-core/src/init/mod.rs | 20 +++-- common/client-core/src/init/types.rs | 83 +++++++++++++------ .../gateway-client/src/client/websockets.rs | 48 +++++------ .../20251126120000_remove_aes128ctr_key.sql | 7 +- common/topology/src/node.rs | 12 ++- nym-registration-client/src/builder/config.rs | 1 + sdk/rust/nym-sdk/src/mixnet/client.rs | 18 ++++ 10 files changed, 129 insertions(+), 65 deletions(-) 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 ed0c2a8ccb1..7571d7edcdf 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:?}"); 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/error.rs b/common/client-core/src/error.rs index ce1cc12f76a..44eca8ac0bb 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -46,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), diff --git a/common/client-core/src/init/mod.rs b/common/client-core/src/init/mod.rs index 5461950c694..fde1ea021fd 100644 --- a/common/client-core/src/init/mod.rs +++ b/common/client-core/src/init/mod.rs @@ -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, @@ -150,6 +157,7 @@ pub async fn update_gateway_details( mut registration: GatewayRegistration, available_gateways: Vec, must_use_tls: bool, + no_hostname: bool, ) -> Result<(), ClientCoreError> where D: GatewaysDetailsStore, @@ -159,7 +167,7 @@ where 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)?; + let selected_gateway = SelectedGateway::from_topology_node(gateway, must_use_tls, no_hostname)?; let new_gateway_listeners = match selected_gateway { SelectedGateway::Remote { diff --git a/common/client-core/src/init/types.rs b/common/client-core/src/init/types.rs index e0955c0e8ee..96fc014bcc8 100644 --- a/common/client-core/src/init/types.rs +++ b/common/client-core/src/init/types.rs @@ -39,34 +39,47 @@ 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 if no_hostname { + // First IP address for main, second if it exists for fallback + let primary = node.ws_entry_address_no_hostname(prefer_ipv6, 0).ok_or( + ClientCoreError::MissingIpAddress(node.identity_key.to_base58_string()), + )?; + let fallback = node.ws_entry_address_no_hostname(prefer_ipv6, 1); + (primary, fallback) } else { - node.ws_entry_address(prefer_ipv6) - .ok_or(ClientCoreError::UnsupportedEntry { - id: node.node_id, - identity: node.identity_key.to_base58_string(), - })? + // WS hostname main, IP address fallback + let primary = + node.ws_entry_address(prefer_ipv6) + .ok_or(ClientCoreError::UnsupportedEntry { + id: node.node_id, + identity: node.identity_key.to_base58_string(), + })?; + let fallback = node.ws_entry_address_no_hostname(prefer_ipv6, 0); + (primary, fallback) }; - let fallback_listener = - node.ws_entry_address_no_hostname(prefer_ipv6) - .and_then(|address| { - Url::parse(&address) - .inspect_err(|err| { - tracing::warn!("Malformed fallback listener, none will be used : {err}") - }) - .ok() - }); - - 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, @@ -76,8 +89,8 @@ impl SelectedGateway { Ok(SelectedGateway::Remote { gateway_id: node.identity_key, gateway_listeners: GatewayListeners { - primary: gateway_listener, - fallback: fallback_listener, + primary: gateway_listener_url, + fallback: fallback_listener_url, }, }) } @@ -168,15 +181,22 @@ impl InitialisationResult { #[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, }, @@ -192,6 +212,7 @@ impl Default for GatewaySelectionSpecification { fn default() -> Self { GatewaySelectionSpecification::UniformRemote { must_use_tls: false, + no_hostname: false, } } } @@ -201,16 +222,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, + } } } } diff --git a/common/client-libs/gateway-client/src/client/websockets.rs b/common/client-libs/gateway-client/src/client/websockets.rs index 474f2a216b7..3162c0343fa 100644 --- a/common/client-libs/gateway-client/src/client/websockets.rs +++ b/common/client-libs/gateway-client/src/client/websockets.rs @@ -15,8 +15,6 @@ use url::{Host, Url}; use std::{net::SocketAddr, time::Duration}; -const INITIAL_CONNECTION_TIMEOUT: Duration = Duration::from_secs(5); - #[cfg(not(target_arch = "wasm32"))] pub(crate) async fn connect_async( endpoint: &str, @@ -40,6 +38,8 @@ pub(crate) async fn connect_async( Host::Ipv6(addr) => vec![SocketAddr::new(addr.into(), port)], Host::Domain(domain) => { // Do a DNS lookup for the domain using our custom DNS resolver + println!("I'M TIRED BOSS"); + tokio::time::sleep(Duration::from_secs(20)).await; resolver .resolve_str(domain) .await? @@ -95,33 +95,29 @@ pub(crate) async fn connect_async_with_fallback( endpoints: &GatewayListeners, #[cfg(unix)] connection_fd_callback: Option>, ) -> Result<(WebSocketStream>, Response), GatewayClientError> { - // Hickory DNS has a non cofigurable 2 * 10 seconds timeout so we have to add one here as well - match tokio::time::timeout( - INITIAL_CONNECTION_TIMEOUT, - connect_async( - endpoints.primary.as_ref(), - #[cfg(unix)] - connection_fd_callback.clone(), - ), + match connect_async( + endpoints.primary.as_ref(), + #[cfg(unix)] + connection_fd_callback.clone(), ) .await { - Ok(inner) => inner, - Err(_) if endpoints.fallback.is_some() => { - // SAFTEY: We know there is a fallback here - #[allow(clippy::unwrap_used)] - let fallback = endpoints.fallback.as_ref().unwrap().to_string(); - tracing::warn!( - "Timeout trying to connect to endpoint {}, trying fallback : {fallback}", - endpoints.primary - ); - connect_async( - &fallback, - #[cfg(unix)] - connection_fd_callback, - ) - .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) + } } - Err(_) => Err(GatewayClientError::Timeout), } } diff --git a/common/gateway-storage/migrations/20251126120000_remove_aes128ctr_key.sql b/common/gateway-storage/migrations/20251126120000_remove_aes128ctr_key.sql index a1917086839..3ecb33ebda7 100644 --- a/common/gateway-storage/migrations/20251126120000_remove_aes128ctr_key.sql +++ b/common/gateway-storage/migrations/20251126120000_remove_aes128ctr_key.sql @@ -9,11 +9,12 @@ 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 + 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) -SELECT client_id, client_address_bs58, derived_aes256_gcm_siv_key +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; diff --git a/common/topology/src/node.rs b/common/topology/src/node.rs index 29bf5bf7d58..9cbb636b81e 100644 --- a/common/topology/src/node.rs +++ b/common/topology/src/node.rs @@ -89,14 +89,20 @@ impl RoutingNode { self.ws_entry_address_no_tls(prefer_ipv6) } - pub fn ws_entry_address_no_hostname(&self, prefer_ipv6: bool) -> Option { + pub fn ws_entry_address_no_hostname(&self, prefer_ipv6: bool, index: usize) -> Option { let entry = self.entry.as_ref()?; - if prefer_ipv6 && let Some(ipv6) = entry.ip_addresses.iter().find(|ip| ip.is_ipv6()) { + if prefer_ipv6 + && let Some(ipv6) = entry + .ip_addresses + .iter() + .filter(|ip| ip.is_ipv6()) + .nth(index) + { return Some(format!("ws://{ipv6}:{}", entry.clients_ws_port)); } - let any_ip = entry.ip_addresses.first()?; + let any_ip = entry.ip_addresses.get(index)?; Some(format!("ws://{any_ip}:{}", entry.clients_ws_port)) } 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/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index 7b1403d0a77..a674fec5e5e 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -57,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>, @@ -102,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, @@ -135,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, @@ -159,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, @@ -230,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 { @@ -342,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() { @@ -394,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, @@ -462,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, @@ -581,6 +597,7 @@ where self.config.user_chosen_gateway.clone(), None, self.force_tls, + self.no_hostname, ); let available_gateways = self.available_gateways().await?; @@ -603,6 +620,7 @@ where gateway_registration, available_gateways, self.force_tls, + self.no_hostname, ) .await } From 371441b2917d889a5de8aaf8eb43764f9716795f Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Fri, 28 Nov 2025 10:17:44 +0100 Subject: [PATCH 6/9] fix wasm client? --- common/wasm/client-core/src/helpers.rs | 3 ++- common/wasm/client-core/src/storage/core_client_traits.rs | 8 ++++++++ common/wasm/client-core/src/storage/types.rs | 5 +++++ 3 files changed, 15 insertions(+), 1 deletion(-) 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 928736e8728..7db4921038b 100644 --- a/common/wasm/client-core/src/storage/core_client_traits.rs +++ b/common/wasm/client-core/src/storage/core_client_traits.rs @@ -154,6 +154,14 @@ impl GatewaysDetailsStore for ClientStorage { self.store_registered_gateway(&raw_registration).await } + async fn update_gateway_details( + &self, + details: &GatewayRegistration, + ) -> Result<(), Self::StorageError> { + // Will overwrite value, which is what we want + self.store_gateway_details(details).await + } + async fn remove_gateway_details(&self, gateway_id: &str) -> Result<(), Self::StorageError> { self.remove_registered_gateway(gateway_id).await } diff --git a/common/wasm/client-core/src/storage/types.rs b/common/wasm/client-core/src/storage/types.rs index b6e11919feb..3e16d8802d3 100644 --- a/common/wasm/client-core/src/storage/types.rs +++ b/common/wasm/client-core/src/storage/types.rs @@ -20,6 +20,9 @@ pub struct WasmRawRegisteredGateway { pub derived_aes256_gcm_siv_key: Vec, pub gateway_listener: String, + + #[zeroize(skip)] + pub expiration_timestamp: OffsetDateTime, } impl TryFrom for GatewayRegistration { @@ -32,6 +35,7 @@ impl TryFrom for GatewayRegistration { derived_aes256_gcm_siv_key: value.derived_aes256_gcm_siv_key, gateway_listener: value.gateway_listener, fallback_listener: None, + expiration_timestamp: value.expiration_timestamp, }; let remote: RemoteGatewayDetails = raw_remote.try_into()?; @@ -55,6 +59,7 @@ impl<'a> From<&'a GatewayRegistration> for WasmRawRegisteredGateway { registration_timestamp: value.registration_timestamp, derived_aes256_gcm_siv_key, gateway_listener: remote_details.gateway_listeners.primary.to_string(), + expiration_timestamp: remote_details.expiration_timestamp, } } } From 470a43f4c1a2942b97205540838c1d5f69481e49 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Fri, 28 Nov 2025 12:12:24 +0100 Subject: [PATCH 7/9] non fallback fixed --- ...51126120000_remove_legacy_add_fallback.sql | 7 +++-- .../src/backend/fs_backend/manager.rs | 11 +++++--- .../client-core/src/client/base_client/mod.rs | 3 +-- .../gateway-client/src/client/websockets.rs | 4 +-- .../client-libs/gateway-client/src/error.rs | 2 +- .../gateway-client/src/socket_state.rs | 5 ---- .../src/registration/handshake/client.rs | 2 +- .../src/registration/handshake/messages.rs | 2 +- .../src/registration/handshake/state.rs | 2 +- .../src/{shared_key/mod.rs => shared_key.rs} | 0 common/gateway-requests/src/types/helpers.rs | 27 ++++++++----------- .../src/types/text_response.rs | 1 - .../websocket/connection_handler/fresh.rs | 11 ++++---- 13 files changed, 34 insertions(+), 43 deletions(-) rename common/gateway-requests/src/{shared_key/mod.rs => shared_key.rs} (100%) 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 index 95b8abe139e..c7003f86e59 100644 --- 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 @@ -9,11 +9,14 @@ CREATE TABLE remote_gateway_details_temp derived_aes256_gcm_siv_key BLOB NOT NULL, gateway_listener TEXT NOT NULL, fallback_listener TEXT, - expiration_timestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL + 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; -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; +-- 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); DROP TABLE remote_gateway_details; ALTER TABLE remote_gateway_details_temp RENAME TO remote_gateway_details; 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 ce61d7b0ad7..cd0d90fec06 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 @@ -144,10 +144,13 @@ impl StorageManager { &self, gateway_id: &str, ) -> Result { - sqlx::query_as("SELECT * FROM remote_gateway_details WHERE gateway_id_bs58 = ?") - .bind(gateway_id) - .fetch_one(&self.connection_pool) - .await + sqlx::query_as!( + RawRemoteGatewayDetails, + "SELECT * FROM remote_gateway_details WHERE gateway_id_bs58 = ?", + gateway_id + ) + .fetch_one(&self.connection_pool) + .await } pub(crate) async fn set_remote_gateway_details( diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index 63cfc4ac53b..77d8bb25203 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -584,10 +584,9 @@ where // the gateway client startup procedure is slightly more complicated now // we need to: // - perform handshake (reg or auth) - // - check for key upgrade // - check for bandwidth // - start background tasks - let _auth_res = gateway_client + let _ = gateway_client .perform_initial_authentication() .await .map_err(gateway_failure)?; diff --git a/common/client-libs/gateway-client/src/client/websockets.rs b/common/client-libs/gateway-client/src/client/websockets.rs index 3162c0343fa..147f99b8a43 100644 --- a/common/client-libs/gateway-client/src/client/websockets.rs +++ b/common/client-libs/gateway-client/src/client/websockets.rs @@ -13,7 +13,7 @@ use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; use tungstenite::handshake::client::Response; use url::{Host, Url}; -use std::{net::SocketAddr, time::Duration}; +use std::net::SocketAddr; #[cfg(not(target_arch = "wasm32"))] pub(crate) async fn connect_async( @@ -38,8 +38,6 @@ pub(crate) async fn connect_async( Host::Ipv6(addr) => vec![SocketAddr::new(addr.into(), port)], Host::Domain(domain) => { // Do a DNS lookup for the domain using our custom DNS resolver - println!("I'M TIRED BOSS"); - tokio::time::sleep(Duration::from_secs(20)).await; resolver .resolve_str(domain) .await? diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index 532296eab23..e0098af50e9 100644 --- a/common/client-libs/gateway-client/src/error.rs +++ b/common/client-libs/gateway-client/src/error.rs @@ -119,7 +119,7 @@ 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:?}")] + #[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( diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index a82e2067b72..1a464aa3045 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -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"); } diff --git a/common/gateway-requests/src/registration/handshake/client.rs b/common/gateway-requests/src/registration/handshake/client.rs index b7a2264336f..9d2584a4a35 100644 --- a/common/gateway-requests/src/registration/handshake/client.rs +++ b/common/gateway-requests/src/registration/handshake/client.rs @@ -21,7 +21,7 @@ impl State<'_, S, R> { 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 + // LOCAL_ID_PUBKEY || EPHEMERAL_KEY || SALT let init_message = self.init_message(hkdf_salt.clone()); self.send_handshake_data(init_message).await?; diff --git a/common/gateway-requests/src/registration/handshake/messages.rs b/common/gateway-requests/src/registration/handshake/messages.rs index d5f6868b871..a693ec5f430 100644 --- a/common/gateway-requests/src/registration/handshake/messages.rs +++ b/common/gateway-requests/src/registration/handshake/messages.rs @@ -61,7 +61,7 @@ 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 { diff --git a/common/gateway-requests/src/registration/handshake/state.rs b/common/gateway-requests/src/registration/handshake/state.rs index a8b91513854..540773243ad 100644 --- a/common/gateway-requests/src/registration/handshake/state.rs +++ b/common/gateway-requests/src/registration/handshake/state.rs @@ -110,7 +110,7 @@ impl<'a, S, R> State<'a, S, R> { 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: Vec) -> Initialisation { diff --git a/common/gateway-requests/src/shared_key/mod.rs b/common/gateway-requests/src/shared_key.rs similarity index 100% rename from common/gateway-requests/src/shared_key/mod.rs rename to common/gateway-requests/src/shared_key.rs diff --git a/common/gateway-requests/src/types/helpers.rs b/common/gateway-requests/src/types/helpers.rs index c915598523f..9f326b1600b 100644 --- a/common/gateway-requests/src/types/helpers.rs +++ b/common/gateway-requests/src/types/helpers.rs @@ -8,7 +8,7 @@ use crate::{ 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,21 +16,18 @@ 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) -> 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() - } + 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 @@ -59,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()..], }) } @@ -76,7 +73,7 @@ impl<'a> BinaryData<'a> { Ok(BinaryData { kind, encrypted: true, - maybe_nonce: Some(&nonce), + nonce: &nonce, data: &ciphertext, } .into_raw()) @@ -91,8 +88,7 @@ impl<'a> BinaryData<'a> { .ok_or(GatewayRequestsError::UnknownRequestKind { kind: self.kind })?; let plaintext = if self.encrypted { - let raw_nonce = self.maybe_nonce.unwrap_or(&[]); - let nonce = SharedSymmetricKey::validate_aead_nonce(raw_nonce)?; + let nonce = SharedSymmetricKey::validate_aead_nonce(self.nonce)?; &*key.decrypt(self.data, &nonce)? } else { @@ -111,8 +107,7 @@ impl<'a> BinaryData<'a> { .ok_or(GatewayRequestsError::UnknownResponseKind { kind: self.kind })?; let plaintext = if self.encrypted { - let raw_nonce = self.maybe_nonce.unwrap_or(&[]); - let nonce = SharedSymmetricKey::validate_aead_nonce(raw_nonce)?; + let nonce = SharedSymmetricKey::validate_aead_nonce(self.nonce)?; &*key.decrypt(self.data, &nonce)? } else { diff --git a/common/gateway-requests/src/types/text_response.rs b/common/gateway-requests/src/types/text_response.rs index 1792ee5a1cb..828c55c15fc 100644 --- a/common/gateway-requests/src/types/text_response.rs +++ b/common/gateway-requests/src/types/text_response.rs @@ -12,7 +12,6 @@ use tungstenite::Message; #[derive(Serialize, Deserialize, Debug)] #[non_exhaustive] pub enum SensitiveServerResponse { - KeyUpgradeAck {}, ForgetMeAck {}, RememberMeAck {}, } 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 cc226c144ca..7547250dd1d 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -80,7 +80,7 @@ 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:?}")] + #[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}")] @@ -672,10 +672,6 @@ impl FreshHandler { S: AsyncRead + AsyncWrite + Unpin + Send, R: CryptoRng + RngCore + Send, { - let negotiated_protocol = self.negotiate_proposed_protocol(client_protocol_version)?; - // populate the negotiated protocol for future uses - self.negotiated_protocol = Some(negotiated_protocol); - let remote_identity = Self::extract_remote_identity_from_register_init(&init_data)?; let remote_address = remote_identity.derive_destination_address(); @@ -694,6 +690,9 @@ impl FreshHandler { .await?; let shared_keys = handshake_result.derived_key; + // populate the negotiated protocol for future uses + self.negotiated_protocol = Some(handshake_result.negotiated_protocol); + let client_id = self.register_client(remote_address, &shared_keys).await?; debug!(client_id = %client_id, "managed to finalize client registration"); @@ -710,7 +709,7 @@ impl FreshHandler { Ok(InitialAuthResult::new( Some(client_details), ServerResponse::Register { - protocol_version: negotiated_protocol, + protocol_version: handshake_result.negotiated_protocol, status: true, upgrade_mode, }, From c30c0fd8ad8314e705f84947796f879abf37466b Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Fri, 28 Nov 2025 14:34:58 +0100 Subject: [PATCH 8/9] improve gateway details update --- .../src/backend/fs_backend/manager.rs | 32 ++--- .../src/backend/fs_backend/mod.rs | 22 ++- .../src/backend/mem_backend.rs | 15 +- .../client-core/gateways-storage/src/error.rs | 8 ++ .../client-core/gateways-storage/src/lib.rs | 5 +- .../client-core/gateways-storage/src/types.rs | 131 +++++++++++------- .../src/cli_helpers/client_add_gateway.rs | 4 +- .../src/cli_helpers/client_list_gateways.rs | 4 +- .../client-core/src/client/base_client/mod.rs | 2 +- .../src/client/base_client/storage/helpers.rs | 11 +- common/client-core/src/init/mod.rs | 20 +-- common/client-core/src/init/types.rs | 5 +- .../gateway-client/src/client/mod.rs | 15 +- .../src/storage/core_client_traits.rs | 16 ++- common/wasm/client-core/src/storage/types.rs | 37 +++-- .../examples/manually_handle_storage.rs | 6 +- sdk/rust/nym-sdk/src/mixnet/client.rs | 8 +- wasm/node-tester/src/tester.rs | 46 +++--- 18 files changed, 234 insertions(+), 153 deletions(-) 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 cd0d90fec06..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( @@ -164,27 +163,28 @@ impl StorageManager { "#, remote.gateway_id_bs58, remote.derived_aes256_gcm_siv_key, - remote.gateway_listener, - remote.fallback_listener, - remote.expiration_timestamp + 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_details( + pub(crate) async fn update_remote_gateway_published_data( &self, - remote: &RawRemoteGatewayDetails, + gateway_id_bs58: &str, + published_data: &RawGatewayPublishedData, ) -> Result<(), sqlx::Error> { sqlx::query!( r#" UPDATE remote_gateway_details SET gateway_listener = ?, fallback_listener = ?, expiration_timestamp = ? WHERE gateway_id_bs58 = ? "#, - remote.gateway_listener, - remote.fallback_listener, - remote.expiration_timestamp, - remote.gateway_id_bs58 + published_data.gateway_listener, + published_data.fallback_listener, + published_data.expiration_timestamp, + gateway_id_bs58 ) .execute(&self.connection_pool) .await?; 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 0a158bc47a0..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,8 +2,8 @@ // 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; @@ -132,20 +132,14 @@ impl GatewaysDetailsStore for OnDiskGatewaysDetails { Ok(()) } - async fn update_gateway_details( + async fn update_gateway_published_data( &self, - details: &GatewayRegistration, + gateway_id: &str, + published_data: &GatewayPublishedData, ) -> Result<(), Self::StorageError> { - match &details.details { - GatewayDetails::Remote(remote_details) => { - let raw_details = &remote_details.into(); - self.manager - .update_remote_gateway_details(raw_details) - .await?; - } - GatewayDetails::Custom(_) => {} - } - + self.manager + .update_remote_gateway_published_data(gateway_id, &published_data.into()) + .await?; Ok(()) } 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 cc7fd156142..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,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::types::{ActiveGateway, GatewayRegistration}; -use crate::{BadGateway, GatewaysDetailsStore}; +use crate::{BadGateway, GatewayDetails, GatewayPublishedData, GatewaysDetailsStore}; use async_trait::async_trait; use std::collections::HashMap; use std::sync::Arc; @@ -94,12 +94,17 @@ impl GatewaysDetailsStore for InMemGatewaysDetails { Ok(()) } - // It will overwrite the existing entry, which is what we ultimately want - async fn update_gateway_details( + async fn update_gateway_published_data( &self, - details: &GatewayRegistration, + gateway_id: &str, + published_data: &GatewayPublishedData, ) -> Result<(), Self::StorageError> { - self.store_gateway_details(details).await?; + let mut guard = self.inner.write().await; + 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 bf87be94b28..3cbb4aa3ddc 100644 --- a/common/client-core/gateways-storage/src/error.rs +++ b/common/client-core/gateways-storage/src/error.rs @@ -40,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 870b69f0a26..08c077e74b9 100644 --- a/common/client-core/gateways-storage/src/lib.rs +++ b/common/client-core/gateways-storage/src/lib.rs @@ -60,9 +60,10 @@ pub trait GatewaysDetailsStore { ) -> Result<(), Self::StorageError>; /// Update the gateway details - async fn update_gateway_details( + async fn update_gateway_published_data( &self, - details: &GatewayRegistration, + 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 e9703d67c01..c5901879536 100644 --- a/common/client-core/gateways-storage/src/types.rs +++ b/common/client-core/gateways-storage/src/types.rs @@ -67,13 +67,12 @@ impl GatewayDetails { pub fn new_remote( gateway_id: ed25519::PublicKey, shared_key: Arc, - gateway_listeners: GatewayListeners, + published_data: GatewayPublishedData, ) -> Self { GatewayDetails::Remote(RemoteGatewayDetails { gateway_id, shared_key, - gateway_listeners, - expiration_timestamp: OffsetDateTime::now_utc() + GATEWAY_DETAILS_TTL, + published_data, }) } @@ -97,20 +96,20 @@ impl GatewayDetails { pub fn details_exipration(&self) -> Option { match self { - GatewayDetails::Remote(details) => Some(details.expiration_timestamp), + GatewayDetails::Remote(details) => Some(details.published_data.expiration_timestamp), GatewayDetails::Custom(_) => None, } } - pub fn update_remote_listeners(&mut self, new_listeners: GatewayListeners) { - match self { - GatewayDetails::Remote(details) => { - details.gateway_listeners = new_listeners; - details.expiration_timestamp = OffsetDateTime::now_utc() + GATEWAY_DETAILS_TTL; - } - GatewayDetails::Custom(_) => {} - } - } + // pub fn update_remote_listeners(&mut self, new_listeners: GatewayListeners) { + // match self { + // GatewayDetails::Remote(details) => { + // details.gateway_listeners = new_listeners; + // details.expiration_timestamp = OffsetDateTime::now_utc() + GATEWAY_DETAILS_TTL; + // } + // GatewayDetails::Custom(_) => {} + // } + // } pub fn is_custom(&self) -> bool { matches!(self, GatewayDetails::Custom(..)) @@ -181,15 +180,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_aes256_gcm_siv_key: Vec, - pub gateway_listener: String, - pub fallback_listener: Option, #[zeroize(skip)] - pub expiration_timestamp: OffsetDateTime, + #[cfg_attr(feature = "sqlx", sqlx(flatten))] + pub published_data: RawGatewayPublishedData, } impl TryFrom for RemoteGatewayDetails { @@ -210,33 +272,10 @@ impl TryFrom for RemoteGatewayDetails { source, })?; - let gateway_listener = Url::parse(&value.gateway_listener).map_err(|source| { - BadGateway::MalformedListener { - gateway_id: value.gateway_id_bs58.clone(), - raw_listener: value.gateway_listener.clone(), - source, - } - })?; - let fallback_listener = value - .fallback_listener - .as_ref() - .map(|uri| { - Url::parse(uri).map_err(|source| BadGateway::MalformedListener { - gateway_id: value.gateway_id_bs58.clone(), - raw_listener: uri.to_owned(), - source, - }) - }) - .transpose()?; - Ok(RemoteGatewayDetails { gateway_id, shared_key: Arc::new(shared_key), - gateway_listeners: GatewayListeners { - primary: gateway_listener, - fallback: fallback_listener, - }, - expiration_timestamp: value.expiration_timestamp, + published_data: value.published_data.clone().try_into()?, }) } } @@ -246,13 +285,7 @@ impl<'a> From<&'a RemoteGatewayDetails> for RawRemoteGatewayDetails { RawRemoteGatewayDetails { gateway_id_bs58: value.gateway_id.to_base58_string(), derived_aes256_gcm_siv_key: value.shared_key.to_bytes(), - gateway_listener: value.gateway_listeners.primary.to_string(), - fallback_listener: value - .gateway_listeners - .fallback - .as_ref() - .map(|uri| uri.to_string()), - expiration_timestamp: value.expiration_timestamp, + published_data: (&value.published_data).into(), } } } @@ -263,9 +296,7 @@ pub struct RemoteGatewayDetails { pub shared_key: Arc, - pub gateway_listeners: GatewayListeners, - - pub expiration_timestamp: OffsetDateTime, + 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 7571d7edcdf..5a1950a35aa 100644 --- a/common/client-core/src/cli_helpers/client_add_gateway.rs +++ b/common/client-core/src/cli_helpers/client_add_gateway.rs @@ -168,7 +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_listeners.primary.clone()), - fallback_endpoint: gateway_details.gateway_listeners.fallback.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_list_gateways.rs b/common/client-core/src/cli_helpers/client_list_gateways.rs index 126397aa177..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,8 +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_listeners.primary), - fallback_endpoint: remote_details.gateway_listeners.fallback, + 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, diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index 77d8bb25203..600085b4cf8 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -554,7 +554,7 @@ where shutdown_tracker.clone_shutdown_token(), ) } else { - let cfg = GatewayConfig::new(details.gateway_id, details.gateway_listeners); + 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) 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 ff06ed4c9dc..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,16 +87,17 @@ where }) } -pub async fn update_stored_gateway_details( +pub async fn update_stored_published_data_gateway( details_store: &D, - details: &GatewayRegistration, + gateway_id: &str, + published_data: &GatewayPublishedData, ) -> Result<(), ClientCoreError> where D: GatewaysDetailsStore, D::StorageError: Send + Sync + 'static, { details_store - .update_gateway_details(details) + .update_gateway_published_data(gateway_id, published_data) .await .map_err(|source| ClientCoreError::GatewaysDetailsStoreError { source: Box::new(source), diff --git a/common/client-core/src/init/mod.rs b/common/client-core/src/init/mod.rs index fde1ea021fd..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, update_stored_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; @@ -127,7 +127,11 @@ where ) .await?; ( - GatewayDetails::new_remote(gateway_id, registration.shared_keys, gateway_listeners), + GatewayDetails::new_remote( + gateway_id, + registration.shared_keys, + GatewayPublishedData::new(gateway_listeners), + ), Some(registration.authenticated_ephemeral_client), ) } @@ -152,9 +156,9 @@ where }) } -pub async fn update_gateway_details( +pub async fn refresh_gateway_published_data( details_store: &D, - mut registration: GatewayRegistration, + registration: GatewayRegistration, available_gateways: Vec, must_use_tls: bool, no_hostname: bool, @@ -179,12 +183,10 @@ where } }; - registration - .details - .update_remote_listeners(new_gateway_listeners); + let new_published_data = GatewayPublishedData::new(new_gateway_listeners); // update gateway details - update_stored_gateway_details(details_store, ®istration).await?; + update_stored_published_data_gateway(details_store, &gateway_id, &new_published_data).await?; Ok(()) } diff --git a/common/client-core/src/init/types.rs b/common/client-core/src/init/types.rs index 96fc014bcc8..c09aa2596ca 100644 --- a/common/client-core/src/init/types.rs +++ b/common/client-core/src/init/types.rs @@ -380,9 +380,10 @@ 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_listeners.primary.to_string(), + gateway_listener: gateway.published_data.listeners.primary.to_string(), fallback_listener: gateway - .gateway_listeners + .published_data + .listeners .fallback .as_ref() .map(|uri| uri.to_string()), diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index ec8b7e58030..bc81e05b6e0 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -199,10 +199,17 @@ 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_addresses.primary - ); + 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)] 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 7db4921038b..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,7 +8,9 @@ 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; @@ -154,12 +156,16 @@ impl GatewaysDetailsStore for ClientStorage { self.store_registered_gateway(&raw_registration).await } - async fn update_gateway_details( + async fn update_gateway_published_data( &self, - details: &GatewayRegistration, + gateway_id: &str, + published_data: &GatewayPublishedData, ) -> Result<(), Self::StorageError> { - // Will overwrite value, which is what we want - self.store_gateway_details(details).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 3e16d8802d3..959add60e07 100644 --- a/common/wasm/client-core/src/storage/types.rs +++ b/common/wasm/client-core/src/storage/types.rs @@ -2,7 +2,8 @@ // 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 serde::{Deserialize, Serialize}; use time::OffsetDateTime; @@ -19,10 +20,8 @@ pub struct WasmRawRegisteredGateway { #[serde(default)] pub derived_aes256_gcm_siv_key: Vec, - pub gateway_listener: String, - #[zeroize(skip)] - pub expiration_timestamp: OffsetDateTime, + pub published_data: WasmRawGatewayPublishedData, } impl TryFrom for GatewayRegistration { @@ -33,9 +32,11 @@ impl TryFrom for GatewayRegistration { let raw_remote = RawRemoteGatewayDetails { gateway_id_bs58: value.gateway_id_bs58, derived_aes256_gcm_siv_key: value.derived_aes256_gcm_siv_key, - gateway_listener: value.gateway_listener, - fallback_listener: None, - expiration_timestamp: value.expiration_timestamp, + 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()?; @@ -58,8 +59,26 @@ impl<'a> From<&'a GatewayRegistration> for WasmRawRegisteredGateway { gateway_id_bs58: remote_details.gateway_id.to_string(), registration_timestamp: value.registration_timestamp, derived_aes256_gcm_siv_key, - gateway_listener: remote_details.gateway_listeners.primary.to_string(), - expiration_timestamp: remote_details.expiration_timestamp, + 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/sdk/rust/nym-sdk/examples/manually_handle_storage.rs b/sdk/rust/nym-sdk/examples/manually_handle_storage.rs index 60544789489..5ae2bbed8d3 100644 --- a/sdk/rust/nym-sdk/examples/manually_handle_storage.rs +++ b/sdk/rust/nym-sdk/examples/manually_handle_storage.rs @@ -1,3 +1,4 @@ +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, @@ -164,9 +165,10 @@ impl GatewaysDetailsStore for MockGatewayDetailsStore { Ok(()) } - async fn update_gateway_details( + async fn update_gateway_published_data( &self, - _details: &GatewayRegistration, + _gateway_id: &str, + _details: &GatewayPublishedData, ) -> Result<(), Self::StorageError> { println!("updating gateway details"); diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index a674fec5e5e..5020c1bd5c8 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -26,7 +26,7 @@ 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::types::{GatewaySelectionSpecification, GatewaySetup}; -use nym_client_core::init::{setup_gateway, update_gateway_details}; +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; @@ -610,12 +610,12 @@ where }) } - async fn update_gateway_details( + async fn refresh_gateway_published_data( &mut self, gateway_registration: GatewayRegistration, ) -> Result<(), ClientCoreError> { let available_gateways = self.available_gateways().await?; - update_gateway_details( + refresh_gateway_published_data( self.storage.gateway_details_store(), gateway_registration, available_gateways, @@ -682,7 +682,7 @@ where // update gateway setup if needed if init_results.exipred_details() { - self.update_gateway_details(init_results.gateway_registration.clone()) + self.refresh_gateway_published_data(init_results.gateway_registration.clone()) .await?; } diff --git a/wasm/node-tester/src/tester.rs b/wasm/node-tester/src/tester.rs index d5cb5fcad87..599e9ce3f99 100644 --- a/wasm/node-tester/src/tester.rs +++ b/wasm/node-tester/src/tester.rs @@ -188,28 +188,30 @@ impl NymNodeTesterBuilder { let stats_sender_task = task_manager.clone_shutdown_token(); - let mut gateway_client = if let Some(existing_client) = - initialisation_result.authenticated_ephemeral_client - { - existing_client.upgrade( - packet_router, - self.bandwidth_controller.take(), - ClientStatsSender::new(None, stats_sender_task), - gateway_task.clone(), - ) - } else { - let cfg = GatewayConfig::new(gateway_info.gateway_id, gateway_info.gateway_listeners); - GatewayClient::new( - GatewayClientConfig::new_default().with_disabled_credentials_mode(true), - cfg, - managed_keys.identity_keypair(), - Some(gateway_info.shared_key), - packet_router, - self.bandwidth_controller.take(), - ClientStatsSender::new(None, stats_sender_task), - gateway_task, - ) - }; + let mut gateway_client = + if let Some(existing_client) = initialisation_result.authenticated_ephemeral_client { + existing_client.upgrade( + packet_router, + self.bandwidth_controller.take(), + ClientStatsSender::new(None, stats_sender_task), + gateway_task.clone(), + ) + } else { + let cfg = GatewayConfig::new( + gateway_info.gateway_id, + gateway_info.published_data.listeners, + ); + GatewayClient::new( + GatewayClientConfig::new_default().with_disabled_credentials_mode(true), + cfg, + managed_keys.identity_keypair(), + Some(gateway_info.shared_key), + packet_router, + self.bandwidth_controller.take(), + ClientStatsSender::new(None, stats_sender_task), + gateway_task, + ) + }; let _auth_res = gateway_client.perform_initial_authentication().await?; From cce2d448dcc069a5a89c411419c13fb932a0f4d9 Mon Sep 17 00:00:00 2001 From: Simon Wicky Date: Fri, 28 Nov 2025 15:50:39 +0100 Subject: [PATCH 9/9] better ws addresses --- common/client-core/src/init/types.rs | 25 +++++--------- common/topology/src/node.rs | 50 ++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 30 deletions(-) diff --git a/common/client-core/src/init/types.rs b/common/client-core/src/init/types.rs index c09aa2596ca..75ee6a6a311 100644 --- a/common/client-core/src/init/types.rs +++ b/common/client-core/src/init/types.rs @@ -52,23 +52,16 @@ impl SelectedGateway { gateway: node.identity_key.to_base58_string(), })?; (primary, None) - } else if no_hostname { - // First IP address for main, second if it exists for fallback - let primary = node.ws_entry_address_no_hostname(prefer_ipv6, 0).ok_or( - ClientCoreError::MissingIpAddress(node.identity_key.to_base58_string()), - )?; - let fallback = node.ws_entry_address_no_hostname(prefer_ipv6, 1); - (primary, fallback) } else { - // WS hostname main, IP address fallback - let primary = - node.ws_entry_address(prefer_ipv6) - .ok_or(ClientCoreError::UnsupportedEntry { - id: node.node_id, - identity: node.identity_key.to_base58_string(), - })?; - let fallback = node.ws_entry_address_no_hostname(prefer_ipv6, 0); - (primary, fallback) + 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 fallback_listener_url = fallback_listener.and_then(|address| { diff --git a/common/topology/src/node.rs b/common/topology/src/node.rs index 9cbb636b81e..b8314c7fb3b 100644 --- a/common/topology/src/node.rs +++ b/common/topology/src/node.rs @@ -89,21 +89,43 @@ impl RoutingNode { self.ws_entry_address_no_tls(prefer_ipv6) } - pub fn ws_entry_address_no_hostname(&self, prefer_ipv6: bool, index: usize) -> Option { - let entry = self.entry.as_ref()?; - - if prefer_ipv6 - && let Some(ipv6) = entry - .ip_addresses - .iter() - .filter(|ip| ip.is_ipv6()) - .nth(index) - { - return Some(format!("ws://{ipv6}:{}", entry.clients_ws_port)); + pub fn ws_entry_address_with_fallback( + &self, + prefer_ipv6: bool, + no_hostname: bool, + ) -> (Option, Option) { + if let Some(entry) = &self.entry { + // 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()) + } else { + (None, None) } - - let any_ip = entry.ip_addresses.get(index)?; - Some(format!("ws://{any_ip}:{}", entry.clients_ws_port)) } pub fn identity(&self) -> ed25519::PublicKey {