From 87567638759d81fb1ca4e5e24058776611111eaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 13 Sep 2024 11:54:12 +0100 Subject: [PATCH 01/17] added support for aead in nym-crypto --- Cargo.lock | 17 ++++ Cargo.toml | 2 + common/bandwidth-controller/Cargo.toml | 2 +- .../client-core/gateways-storage/src/error.rs | 2 +- .../client-core/gateways-storage/src/types.rs | 20 +++-- .../base_client/storage/migration_helpers.rs | 8 +- .../client-core/src/client/key_manager/mod.rs | 4 +- common/client-core/src/init/types.rs | 4 +- .../gateway-client/src/client/mod.rs | 20 +++-- common/client-libs/gateway-client/src/lib.rs | 4 +- .../gateway-client/src/socket_state.rs | 8 +- common/crypto/Cargo.toml | 5 +- common/crypto/src/lib.rs | 11 +-- common/crypto/src/symmetric/aead.rs | 83 +++++++++++++++++ common/crypto/src/symmetric/mod.rs | 3 + common/crypto/src/symmetric/stream_cipher.rs | 3 +- common/gateway-requests/Cargo.toml | 2 +- .../src/authentication/encrypted_address.rs | 15 ++-- common/gateway-requests/src/iv.rs | 10 +-- common/gateway-requests/src/lib.rs | 4 +- .../src/registration/handshake/client.rs | 6 +- .../src/registration/handshake/gateway.rs | 6 +- .../src/registration/handshake/mod.rs | 20 +++-- .../{shared_key.rs => shared_key/legacy.rs} | 54 +++++------ .../registration/handshake/shared_key/mod.rs | 90 +++++++++++++++++++ .../src/registration/handshake/state.rs | 20 ++--- common/gateway-requests/src/types.rs | 28 +++--- common/gateway-storage/src/lib.rs | 6 +- common/nymsphinx/acknowledgements/Cargo.toml | 2 +- common/nymsphinx/anonymous-replies/Cargo.toml | 2 +- common/nymsphinx/params/Cargo.toml | 2 +- common/nymsphinx/params/src/lib.rs | 14 ++- .../websocket/connection_handler/fresh.rs | 19 ++-- .../websocket/connection_handler/mod.rs | 10 ++- 34 files changed, 366 insertions(+), 140 deletions(-) create mode 100644 common/crypto/src/symmetric/aead.rs rename common/gateway-requests/src/registration/handshake/{shared_key.rs => shared_key/legacy.rs} (73%) create mode 100644 common/gateway-requests/src/registration/handshake/shared_key/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 7d9dd13ca7..8395d243d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,6 +86,21 @@ dependencies = [ "subtle 2.5.0", ] +[[package]] +name = "aes-gcm-siv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "polyval", + "subtle 2.5.0", + "zeroize", +] + [[package]] name = "ahash" version = "0.7.8" @@ -4770,7 +4785,9 @@ dependencies = [ name = "nym-crypto" version = "0.4.0" dependencies = [ + "aead", "aes", + "aes-gcm-siv", "blake3", "bs58", "cipher", diff --git a/Cargo.toml b/Cargo.toml index 32a46b44a7..85fc7cb1e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -169,6 +169,8 @@ readme = "README.md" addr = "0.15.6" aes = "0.8.1" aes-gcm = "0.10.1" +aes-gcm-siv = "0.11.1" +aead = "0.5.2" anyhow = "1.0.89" argon2 = "0.5.0" async-trait = "0.1.82" diff --git a/common/bandwidth-controller/Cargo.toml b/common/bandwidth-controller/Cargo.toml index e56476afa6..386489ea7a 100644 --- a/common/bandwidth-controller/Cargo.toml +++ b/common/bandwidth-controller/Cargo.toml @@ -18,7 +18,7 @@ nym-ecash-time = { path = "../ecash-time" } nym-credential-storage = { path = "../credential-storage" } nym-credentials = { path = "../credentials" } nym-credentials-interface = { path = "../credentials-interface" } -nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "symmetric", "aes", "hashing"] } +nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "stream_cipher", "aes", "hashing"] } nym-network-defaults = { path = "../network-defaults" } nym-validator-client = { path = "../client-libs/validator-client", default-features = false } nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" } diff --git a/common/client-core/gateways-storage/src/error.rs b/common/client-core/gateways-storage/src/error.rs index f547164022..c867557108 100644 --- a/common/client-core/gateways-storage/src/error.rs +++ b/common/client-core/gateways-storage/src/error.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use nym_crypto::asymmetric::identity::Ed25519RecoveryError; -use nym_gateway_requests::registration::handshake::shared_key::SharedKeyConversionError; +use nym_gateway_requests::registration::handshake::SharedKeyConversionError; use thiserror::Error; #[derive(Debug, Error)] diff --git a/common/client-core/gateways-storage/src/types.rs b/common/client-core/gateways-storage/src/types.rs index 8daade3422..94888faf9c 100644 --- a/common/client-core/gateways-storage/src/types.rs +++ b/common/client-core/gateways-storage/src/types.rs @@ -4,7 +4,7 @@ use crate::BadGateway; use cosmrs::AccountId; use nym_crypto::asymmetric::identity; -use nym_gateway_requests::registration::handshake::SharedKeys; +use nym_gateway_requests::registration::handshake::LegacySharedKeys; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::str::FromStr; @@ -64,7 +64,7 @@ impl From for GatewayRegistration { impl GatewayDetails { pub fn new_remote( gateway_id: identity::PublicKey, - derived_aes128_ctr_blake3_hmac_keys: Arc, + derived_aes128_ctr_blake3_hmac_keys: Arc, gateway_owner_address: Option, gateway_listener: Url, ) -> Self { @@ -87,7 +87,7 @@ impl GatewayDetails { } } - pub fn shared_key(&self) -> Option<&SharedKeys> { + pub fn shared_key(&self) -> Option<&LegacySharedKeys> { match self { GatewayDetails::Remote(details) => Some(&details.derived_aes128_ctr_blake3_hmac_keys), GatewayDetails::Custom(_) => None, @@ -185,11 +185,13 @@ impl TryFrom for RemoteGatewayDetails { })?; let derived_aes128_ctr_blake3_hmac_keys = Arc::new( - SharedKeys::try_from_base58_string(&value.derived_aes128_ctr_blake3_hmac_keys_bs58) - .map_err(|source| BadGateway::MalformedSharedKeys { - gateway_id: value.gateway_id_bs58.clone(), - source, - })?, + LegacySharedKeys::try_from_base58_string( + &value.derived_aes128_ctr_blake3_hmac_keys_bs58, + ) + .map_err(|source| BadGateway::MalformedSharedKeys { + gateway_id: value.gateway_id_bs58.clone(), + source, + })?, ); let gateway_owner_address = value @@ -242,7 +244,7 @@ pub struct RemoteGatewayDetails { // note: `SharedKeys` implement ZeroizeOnDrop, meaning when `RemoteGatewayDetails` is dropped, // the keys will be zeroized - pub derived_aes128_ctr_blake3_hmac_keys: Arc, + pub derived_aes128_ctr_blake3_hmac_keys: Arc, pub gateway_owner_address: Option, 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 b03b6d13c7..91d7a0bf93 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 @@ -13,7 +13,7 @@ pub mod v1_1_33 { use nym_client_core_gateways_storage::{ CustomGatewayDetails, GatewayDetails, GatewayRegistration, RemoteGatewayDetails, }; - use nym_gateway_requests::registration::handshake::SharedKeys; + use nym_gateway_requests::registration::handshake::LegacySharedKeys; use serde::{Deserialize, Serialize}; use sha2::{digest::Digest, Sha256}; use std::ops::Deref; @@ -58,7 +58,7 @@ pub mod v1_1_33 { } impl PersistedGatewayConfig { - fn verify(&self, shared_key: &SharedKeys) -> bool { + fn verify(&self, shared_key: &LegacySharedKeys) -> bool { let key_bytes = Zeroizing::new(shared_key.to_bytes()); let mut key_hasher = Sha256::new(); @@ -74,7 +74,7 @@ pub mod v1_1_33 { gateway_id: String, } - fn load_shared_key>(path: P) -> Result { + fn load_shared_key>(path: P) -> Result { // the shared key was a simple pem file Ok(nym_pemstore::load_key(path)?) } @@ -83,7 +83,7 @@ pub mod v1_1_33 { gateway_id: String, gateway_owner: String, gateway_listener: String, - gateway_shared_key: SharedKeys, + gateway_shared_key: LegacySharedKeys, ) -> Result { Ok(GatewayDetails::Remote(RemoteGatewayDetails { gateway_id: gateway_id diff --git a/common/client-core/src/client/key_manager/mod.rs b/common/client-core/src/client/key_manager/mod.rs index 095980e753..01eaa88fc3 100644 --- a/common/client-core/src/client/key_manager/mod.rs +++ b/common/client-core/src/client/key_manager/mod.rs @@ -3,7 +3,7 @@ use crate::client::key_manager::persistence::KeyStore; use nym_crypto::asymmetric::{encryption, identity}; -use nym_gateway_requests::registration::handshake::SharedKeys; +use nym_gateway_requests::registration::handshake::LegacySharedKeys; use nym_sphinx::acknowledgements::AckKey; use rand::{CryptoRng, RngCore}; use std::sync::Arc; @@ -84,5 +84,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::(); } diff --git a/common/client-core/src/init/types.rs b/common/client-core/src/init/types.rs index 8f1daa84b4..d0744abf62 100644 --- a/common/client-core/src/init/types.rs +++ b/common/client-core/src/init/types.rs @@ -11,7 +11,7 @@ use nym_client_core_gateways_storage::{ }; use nym_crypto::asymmetric::identity; use nym_gateway_client::client::InitGatewayClient; -use nym_gateway_requests::registration::handshake::SharedKeys; +use nym_gateway_requests::registration::handshake::LegacySharedKeys; use nym_sphinx::addressing::clients::Recipient; use nym_topology::gateway; use nym_validator_client::client::IdentityKey; @@ -104,7 +104,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, } diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index e6e907c307..6f29ea5b0b 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -19,7 +19,7 @@ use nym_credentials::CredentialSpendingData; use nym_crypto::asymmetric::identity; use nym_gateway_requests::authentication::encrypted_address::EncryptedAddressBytes; use nym_gateway_requests::iv::IV; -use nym_gateway_requests::registration::handshake::{client_handshake, SharedKeys}; +use nym_gateway_requests::registration::handshake::{client_handshake, LegacySharedKeys}; use nym_gateway_requests::{ BinaryRequest, ClientControlRequest, ServerResponse, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION, @@ -80,7 +80,7 @@ pub struct GatewayClient { gateway_address: String, gateway_identity: identity::PublicKey, local_identity: Arc, - shared_key: Option>, + shared_key: Option>, connection: SocketState, packet_router: PacketRouter, bandwidth_controller: Option>, @@ -98,7 +98,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>, task_client: TaskClient, @@ -450,7 +450,7 @@ impl GatewayClient { async fn authenticate( &mut self, - shared_key: Option, + shared_key: Option, ) -> Result<(), GatewayClientError> { if shared_key.is_none() && self.shared_key.is_none() { return Err(GatewayClientError::NoSharedKeyAvailable); @@ -508,7 +508,13 @@ impl GatewayClient { /// Helper method to either call register or authenticate based on self.shared_key value pub async fn perform_initial_authentication( &mut self, - ) -> Result, GatewayClientError> { + ) -> Result, GatewayClientError> { + // 1. check gateway's protocol version + // 2. if error or new handshake unsupported => fallback to the old registration/authentication + // 3. otherwise continue with the updated key derivation + + // ?. if new protocol is supported and we have an old key, upgrade it to aes-gcm-siv + if self.authenticated { debug!("Already authenticated"); return if let Some(shared_key) = &self.shared_key { @@ -833,7 +839,9 @@ impl GatewayClient { Ok(()) } - pub async fn authenticate_and_start(&mut self) -> Result, GatewayClientError> + pub async fn authenticate_and_start( + &mut self, + ) -> Result, GatewayClientError> where C: DkgQueryClient + Send + Sync, St: CredentialStorage, diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index e37cb3e4a4..a07e03a258 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -7,7 +7,7 @@ use nym_gateway_requests::BinaryResponse; use tungstenite::{protocol::Message, Error as WsError}; pub use client::{config::GatewayClientConfig, GatewayClient, GatewayConfig}; -pub use nym_gateway_requests::registration::handshake::SharedKeys; +pub use nym_gateway_requests::registration::handshake::LegacySharedKeys; pub use packet_router::{ AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender, PacketRouter, @@ -45,7 +45,7 @@ pub(crate) fn cleanup_socket_messages( pub(crate) fn try_decrypt_binary_message( bin_msg: Vec, - shared_keys: &SharedKeys, + shared_keys: &LegacySharedKeys, ) -> 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 d7754c5341..d8a7e7c747 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 futures::channel::oneshot; use futures::stream::{SplitSink, SplitStream}; use futures::{SinkExt, StreamExt}; use log::*; -use nym_gateway_requests::registration::handshake::SharedKeys; +use nym_gateway_requests::registration::handshake::LegacySharedKeys; use nym_gateway_requests::{ServerResponse, SimpleGatewayRequestsError}; use nym_task::TaskClient; use std::os::raw::c_int as RawFd; @@ -62,7 +62,7 @@ pub(crate) struct PartiallyDelegatedHandle { struct PartiallyDelegatedRouter { packet_router: PacketRouter, - shared_key: Arc, + shared_key: Arc, client_bandwidth: ClientBandwidth, stream_return: SplitStreamSender, @@ -72,7 +72,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<()>, @@ -247,7 +247,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: TaskClient, ) -> Self { diff --git a/common/crypto/Cargo.toml b/common/crypto/Cargo.toml index 8e6da0b419..1dcb0b4957 100644 --- a/common/crypto/Cargo.toml +++ b/common/crypto/Cargo.toml @@ -8,7 +8,9 @@ license = { workspace = true } repository = { workspace = true } [dependencies] +aes-gcm-siv = { workspace = true, optional = true } aes = { workspace = true, optional = true } +aead = { workspace = true, optional = true } bs58 = { workspace = true } blake3 = { workspace = true, features = ["traits-preview"], optional = true } ctr = { workspace = true, optional = true } @@ -35,9 +37,10 @@ rand_chacha = { workspace = true } [features] default = ["sphinx"] +aead = ["dep:aead", "aead/std", "aes-gcm-siv", "generic-array"] serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"] asymmetric = ["x25519-dalek", "ed25519-dalek", "zeroize"] hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array"] -symmetric = ["aes", "ctr", "cipher", "generic-array"] +stream_cipher = ["aes", "ctr", "cipher", "generic-array"] sphinx = ["nym-sphinx-types/sphinx"] outfox = ["nym-sphinx-types/outfox"] diff --git a/common/crypto/src/lib.rs b/common/crypto/src/lib.rs index 4ab59949f0..1dff7b82be 100644 --- a/common/crypto/src/lib.rs +++ b/common/crypto/src/lib.rs @@ -10,21 +10,22 @@ pub mod crypto_hash; pub mod hkdf; #[cfg(feature = "hashing")] pub mod hmac; -#[cfg(all(feature = "asymmetric", feature = "hashing", feature = "symmetric"))] +#[cfg(all(feature = "asymmetric", feature = "hashing", feature = "stream_cipher"))] pub mod shared_key; -#[cfg(feature = "symmetric")] pub mod symmetric; #[cfg(feature = "hashing")] pub use digest::{Digest, OutputSizeUser}; -#[cfg(any(feature = "hashing", feature = "symmetric"))] +#[cfg(any(feature = "hashing", feature = "stream_cipher", feature = "aead"))] pub use generic_array; // with the below my idea was to try to introduce having a single place of importing all hashing, encryption, // etc. algorithms and import them elsewhere as needed via common/crypto -#[cfg(feature = "symmetric")] +#[cfg(feature = "stream_cipher")] pub use aes; +#[cfg(feature = "aead")] +pub use aes_gcm_siv::{Aes128GcmSiv, Aes256GcmSiv}; #[cfg(feature = "hashing")] pub use blake3; -#[cfg(feature = "symmetric")] +#[cfg(feature = "stream_cipher")] pub use ctr; diff --git a/common/crypto/src/symmetric/aead.rs b/common/crypto/src/symmetric/aead.rs new file mode 100644 index 0000000000..6081d4c9c2 --- /dev/null +++ b/common/crypto/src/symmetric/aead.rs @@ -0,0 +1,83 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use aead::{Aead, AeadCore, AeadInPlace, Buffer, KeyInit, Payload}; + +#[cfg(feature = "rand")] +use rand::{CryptoRng, RngCore}; + +pub use aead::{Error as AeadError, Key as AeadKey, KeySizeUser, Nonce, Tag}; + +#[cfg(feature = "rand")] +pub fn generate_key(rng: &mut R) -> AeadKey +where + A: KeyInit, + R: RngCore + CryptoRng, +{ + let mut key = AeadKey::::default(); + rng.fill_bytes(&mut key); + key +} + +#[cfg(feature = "rand")] +pub fn random_nonce(rng: &mut R) -> Nonce +where + A: AeadCore, + R: RngCore + CryptoRng, +{ + ::generate_nonce(rng) +} + +#[inline] +pub fn encrypt<'msg, 'aad, A>( + key: &AeadKey, + nonce: &Nonce, + plaintext: impl Into>, +) -> Result, AeadError> +where + A: Aead + KeyInit, +{ + let cipher = A::new(key); + cipher.encrypt(nonce, plaintext) +} + +#[inline] +pub fn decrypt<'msg, 'aad, A>( + key: &AeadKey, + nonce: &Nonce, + ciphertext: impl Into>, +) -> Result, AeadError> +where + A: Aead + KeyInit, +{ + let cipher = A::new(key); + cipher.decrypt(nonce, ciphertext) +} + +#[inline] +pub fn encrypt_in_place( + key: &AeadKey, + nonce: &Nonce, + associated_data: &[u8], + buffer: &mut dyn Buffer, +) -> Result<(), AeadError> +where + A: AeadInPlace + KeyInit, +{ + let cipher = A::new(key); + cipher.encrypt_in_place(nonce, associated_data, buffer) +} + +#[inline] +pub fn decrypt_in_place( + key: &AeadKey, + nonce: &Nonce, + associated_data: &[u8], + buffer: &mut dyn Buffer, +) -> Result<(), AeadError> +where + A: AeadInPlace + KeyInit, +{ + let cipher = A::new(key); + cipher.decrypt_in_place(nonce, associated_data, buffer) +} diff --git a/common/crypto/src/symmetric/mod.rs b/common/crypto/src/symmetric/mod.rs index f61a2e5eb2..26e990b196 100644 --- a/common/crypto/src/symmetric/mod.rs +++ b/common/crypto/src/symmetric/mod.rs @@ -1,4 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +#[cfg(feature = "aead")] +pub mod aead; +#[cfg(feature = "stream_cipher")] pub mod stream_cipher; diff --git a/common/crypto/src/symmetric/stream_cipher.rs b/common/crypto/src/symmetric/stream_cipher.rs index 571ec67f08..c59843050b 100644 --- a/common/crypto/src/symmetric/stream_cipher.rs +++ b/common/crypto/src/symmetric/stream_cipher.rs @@ -2,12 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use cipher::{Iv, StreamCipher}; -pub use cipher::{IvSizeUser, KeyIvInit, KeySizeUser}; + #[cfg(feature = "rand")] use rand::{CryptoRng, RngCore}; // re-export this for ease of use pub use cipher::Key as CipherKey; +pub use cipher::{IvSizeUser, KeyIvInit, KeySizeUser}; // SECURITY: // TODO: note that this is not the most secure approach here diff --git a/common/gateway-requests/Cargo.toml b/common/gateway-requests/Cargo.toml index 72a2bdfe9d..3517df78b2 100644 --- a/common/gateway-requests/Cargo.toml +++ b/common/gateway-requests/Cargo.toml @@ -21,7 +21,7 @@ thiserror = { workspace = true } tracing = { workspace = true, features = ["log"] } zeroize = { workspace = true } -nym-crypto = { path = "../crypto" } +nym-crypto = { path = "../crypto", features = ["aead"] } nym-pemstore = { path = "../pemstore" } nym-sphinx = { path = "../nymsphinx" } nym-task = { path = "../task" } diff --git a/common/gateway-requests/src/authentication/encrypted_address.rs b/common/gateway-requests/src/authentication/encrypted_address.rs index bd500932c7..f061f170a4 100644 --- a/common/gateway-requests/src/authentication/encrypted_address.rs +++ b/common/gateway-requests/src/authentication/encrypted_address.rs @@ -2,9 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::iv::IV; -use crate::registration::handshake::shared_key::SharedKeys; +use crate::registration::handshake::LegacySharedKeys; use nym_crypto::symmetric::stream_cipher; -use nym_sphinx::params::GatewayEncryptionAlgorithm; +use nym_sphinx::params::LegacyGatewayEncryptionAlgorithm; use nym_sphinx::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH}; use thiserror::Error; @@ -28,8 +28,8 @@ pub enum EncryptedAddressConversionError { } impl EncryptedAddressBytes { - pub fn new(address: &DestinationAddressBytes, key: &SharedKeys, iv: &IV) -> Self { - let ciphertext = stream_cipher::encrypt::( + pub fn new(address: &DestinationAddressBytes, key: &LegacySharedKeys, iv: &IV) -> Self { + let ciphertext = stream_cipher::encrypt::( key.encryption_key(), iv.inner(), address.as_bytes_ref(), @@ -40,7 +40,12 @@ impl EncryptedAddressBytes { EncryptedAddressBytes(enc_address) } - pub fn verify(&self, address: &DestinationAddressBytes, key: &SharedKeys, iv: &IV) -> bool { + pub fn verify( + &self, + address: &DestinationAddressBytes, + key: &LegacySharedKeys, + iv: &IV, + ) -> bool { self == &Self::new(address, key, iv) } diff --git a/common/gateway-requests/src/iv.rs b/common/gateway-requests/src/iv.rs index d7ab8d1c19..5db5b320bf 100644 --- a/common/gateway-requests/src/iv.rs +++ b/common/gateway-requests/src/iv.rs @@ -3,15 +3,15 @@ use nym_crypto::generic_array::{typenum::Unsigned, GenericArray}; use nym_crypto::symmetric::stream_cipher::{random_iv, IvSizeUser, IV as CryptoIV}; -use nym_sphinx::params::GatewayEncryptionAlgorithm; +use nym_sphinx::params::LegacyGatewayEncryptionAlgorithm; use rand::{CryptoRng, RngCore}; use thiserror::Error; -type NonceSize = ::IvSize; +type NonceSize = ::IvSize; // I think 'IV' looks better than 'Iv', feel free to change that. #[allow(clippy::upper_case_acronyms)] -pub struct IV(CryptoIV); +pub struct IV(CryptoIV); #[derive(Error, Debug)] // I think 'IV' looks better than 'Iv', feel free to change that. @@ -29,7 +29,7 @@ pub enum IVConversionError { impl IV { pub fn new_random(rng: &mut R) -> Self { - IV(random_iv::(rng)) + IV(random_iv::(rng)) } pub fn try_from_bytes(bytes: &[u8]) -> Result { @@ -48,7 +48,7 @@ impl IV { self.0.as_ref() } - pub fn inner(&self) -> &CryptoIV { + pub fn inner(&self) -> &CryptoIV { &self.0 } diff --git a/common/gateway-requests/src/lib.rs b/common/gateway-requests/src/lib.rs index 98feccc75b..47e30e2758 100644 --- a/common/gateway-requests/src/lib.rs +++ b/common/gateway-requests/src/lib.rs @@ -13,15 +13,17 @@ pub mod models; pub mod registration; pub mod types; -pub const CURRENT_PROTOCOL_VERSION: u8 = CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION; +pub const CURRENT_PROTOCOL_VERSION: u8 = AES_GCM_SIV_PROTOCOL_VERSION; /// Defines the current version of the communication protocol between gateway and clients. /// It has to be incremented for any breaking change. // history: // 1 - initial release // 2 - changes to client credentials structure +// 3 - change to AES-GCM-SIV and non-zero IVs pub const INITIAL_PROTOCOL_VERSION: u8 = 1; pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: u8 = 2; +pub const AES_GCM_SIV_PROTOCOL_VERSION: u8 = 3; pub type GatewayMac = HmacOutput; diff --git a/common/gateway-requests/src/registration/handshake/client.rs b/common/gateway-requests/src/registration/handshake/client.rs index 902a520bca..fe099e6178 100644 --- a/common/gateway-requests/src/registration/handshake/client.rs +++ b/common/gateway-requests/src/registration/handshake/client.rs @@ -1,8 +1,8 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::registration::handshake::shared_key::SharedKeys; use crate::registration::handshake::state::State; +use crate::registration::handshake::LegacySharedKeys; use crate::registration::handshake::{error::HandshakeError, WsItem}; use futures::future::BoxFuture; use futures::task::{Context, Poll}; @@ -15,7 +15,7 @@ use std::pin::Pin; use tungstenite::Message as WsMessage; pub(crate) struct ClientHandshake<'a> { - handshake_future: BoxFuture<'a, Result>, + handshake_future: BoxFuture<'a, Result>, } impl<'a> ClientHandshake<'a> { @@ -121,7 +121,7 @@ impl<'a> ClientHandshake<'a> { } impl<'a> Future for ClientHandshake<'a> { - type Output = Result; + type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { Pin::new(&mut self.handshake_future).poll(cx) diff --git a/common/gateway-requests/src/registration/handshake/gateway.rs b/common/gateway-requests/src/registration/handshake/gateway.rs index a42851a320..52fe886263 100644 --- a/common/gateway-requests/src/registration/handshake/gateway.rs +++ b/common/gateway-requests/src/registration/handshake/gateway.rs @@ -1,8 +1,8 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::registration::handshake::shared_key::SharedKeys; use crate::registration::handshake::state::State; +use crate::registration::handshake::LegacySharedKeys; use crate::registration::handshake::{error::HandshakeError, WsItem}; use futures::future::BoxFuture; use futures::task::{Context, Poll}; @@ -14,7 +14,7 @@ use std::pin::Pin; use tungstenite::Message as WsMessage; pub(crate) struct GatewayHandshake<'a> { - handshake_future: BoxFuture<'a, Result>, + handshake_future: BoxFuture<'a, Result>, } impl<'a> GatewayHandshake<'a> { @@ -106,7 +106,7 @@ impl<'a> GatewayHandshake<'a> { } impl<'a> Future for GatewayHandshake<'a> { - type Output = Result; + type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { Pin::new(&mut self.handshake_future).poll(cx) diff --git a/common/gateway-requests/src/registration/handshake/mod.rs b/common/gateway-requests/src/registration/handshake/mod.rs index d3faec7903..56804f8f55 100644 --- a/common/gateway-requests/src/registration/handshake/mod.rs +++ b/common/gateway-requests/src/registration/handshake/mod.rs @@ -3,25 +3,29 @@ use self::client::ClientHandshake; use self::error::HandshakeError; -#[cfg(not(target_arch = "wasm32"))] -use self::gateway::GatewayHandshake; -pub use self::shared_key::{SharedKeySize, SharedKeys}; use futures::{Sink, Stream}; use nym_crypto::asymmetric::identity; -#[cfg(not(target_arch = "wasm32"))] -use nym_task::TaskClient; use rand::{CryptoRng, RngCore}; use tungstenite::{Error as WsError, Message as WsMessage}; +#[cfg(not(target_arch = "wasm32"))] +use self::gateway::GatewayHandshake; + +#[cfg(not(target_arch = "wasm32"))] +use nym_task::TaskClient; + pub(crate) type WsItem = Result; mod client; pub mod error; #[cfg(not(target_arch = "wasm32"))] mod gateway; -pub mod shared_key; +mod shared_key; mod state; +pub use self::shared_key::legacy::{LegacySharedKeySize, LegacySharedKeys}; +pub use self::shared_key::{SharedKeyConversionError, SharedSymmetricKey}; + // Note: the handshake is built on top of WebSocket, but in principle it shouldn't be too difficult // to remove that restriction, by just changing Sink and Stream into // AsyncWrite and AsyncRead and slightly adjusting the implementation. But right now @@ -34,7 +38,7 @@ pub async fn client_handshake<'a, S>( gateway_pubkey: identity::PublicKey, expects_credential_usage: bool, #[cfg(not(target_arch = "wasm32"))] shutdown: TaskClient, -) -> Result +) -> Result where S: Stream + Sink + Unpin + Send + 'a, { @@ -57,7 +61,7 @@ pub async fn gateway_handshake<'a, S>( identity: &'a identity::KeyPair, received_init_payload: Vec, shutdown: TaskClient, -) -> Result +) -> Result where S: Stream + Sink + Unpin + Send + 'a, { diff --git a/common/gateway-requests/src/registration/handshake/shared_key.rs b/common/gateway-requests/src/registration/handshake/shared_key/legacy.rs similarity index 73% rename from common/gateway-requests/src/registration/handshake/shared_key.rs rename to common/gateway-requests/src/registration/handshake/shared_key/legacy.rs index 64975fe4cd..79c7382cff 100644 --- a/common/gateway-requests/src/registration/handshake/shared_key.rs +++ b/common/gateway-requests/src/registration/handshake/shared_key/legacy.rs @@ -1,6 +1,7 @@ // Copyright 2020-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::registration::handshake::shared_key::SharedKeyConversionError; use crate::{GatewayMacSize, GatewayRequestsError}; use nym_crypto::generic_array::{ typenum::{Sum, Unsigned, U16}, @@ -9,43 +10,32 @@ use nym_crypto::generic_array::{ 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::{GatewayEncryptionAlgorithm, GatewayIntegrityHmacAlgorithm}; +use nym_sphinx::params::{GatewayIntegrityHmacAlgorithm, LegacyGatewayEncryptionAlgorithm}; use serde::{Deserialize, Serialize}; -use thiserror::Error; use zeroize::{Zeroize, ZeroizeOnDrop}; // shared key is as long as the encryption key and the MAC key combined. -pub type SharedKeySize = Sum; +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; +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 SharedKeys { - encryption_key: CipherKey, +pub struct LegacySharedKeys { + encryption_key: CipherKey, mac_key: MacKey, } -#[derive(Debug, Clone, Copy, Error)] -pub enum SharedKeyConversionError { - #[error("the string representation of the shared keys was malformed - {0}")] - DecodeError(#[from] bs58::decode::Error), - #[error( - "the received shared keys had invalid size. Got: {received}, but expected: {expected}" - )] - InvalidSharedKeysSize { received: usize, expected: usize }, -} - -impl SharedKeys { +impl LegacySharedKeys { pub fn try_from_bytes(bytes: &[u8]) -> Result { - if bytes.len() != SharedKeySize::to_usize() { + if bytes.len() != LegacySharedKeySize::to_usize() { return Err(SharedKeyConversionError::InvalidSharedKeysSize { received: bytes.len(), - expected: SharedKeySize::to_usize(), + expected: LegacySharedKeySize::to_usize(), }); } @@ -53,7 +43,7 @@ impl SharedKeys { GenericArray::clone_from_slice(&bytes[..EncryptionKeySize::to_usize()]); let mac_key = GenericArray::clone_from_slice(&bytes[EncryptionKeySize::to_usize()..]); - Ok(SharedKeys { + Ok(LegacySharedKeys { encryption_key, mac_key, }) @@ -65,17 +55,17 @@ impl SharedKeys { pub fn encrypt_and_tag( &self, data: &[u8], - iv: Option<&IV>, + iv: Option<&IV>, ) -> Vec { let encrypted_data = match iv { - Some(iv) => stream_cipher::encrypt::( + Some(iv) => stream_cipher::encrypt::( self.encryption_key(), iv, data, ), None => { - let zero_iv = stream_cipher::zero_iv::(); - stream_cipher::encrypt::( + let zero_iv = stream_cipher::zero_iv::(); + stream_cipher::encrypt::( self.encryption_key(), &zero_iv, data, @@ -93,7 +83,7 @@ impl SharedKeys { pub fn decrypt_tagged( &self, enc_data: &[u8], - iv: Option<&IV>, + iv: Option<&IV>, ) -> Result, GatewayRequestsError> { let mac_size = GatewayMacSize::to_usize(); if enc_data.len() < mac_size { @@ -115,9 +105,9 @@ impl SharedKeys { // together with a mutable one let message_bytes_mut = &mut enc_data.to_vec()[mac_size..]; - let zero_iv = stream_cipher::zero_iv::(); + let zero_iv = stream_cipher::zero_iv::(); let iv = iv.unwrap_or(&zero_iv); - stream_cipher::decrypt_in_place::( + stream_cipher::decrypt_in_place::( self.encryption_key(), iv, message_bytes_mut, @@ -125,7 +115,7 @@ impl SharedKeys { Ok(message_bytes_mut.to_vec()) } - pub fn encryption_key(&self) -> &CipherKey { + pub fn encryption_key(&self) -> &CipherKey { &self.encryption_key } @@ -145,7 +135,7 @@ impl SharedKeys { val: S, ) -> Result { let decoded = bs58::decode(val.into()).into_vec()?; - SharedKeys::try_from_bytes(&decoded) + LegacySharedKeys::try_from_bytes(&decoded) } pub fn to_base58_string(&self) -> String { @@ -153,13 +143,13 @@ impl SharedKeys { } } -impl From for String { - fn from(keys: SharedKeys) -> Self { +impl From for String { + fn from(keys: LegacySharedKeys) -> Self { keys.to_base58_string() } } -impl PemStorableKey for SharedKeys { +impl PemStorableKey for LegacySharedKeys { type Error = SharedKeyConversionError; fn pem_type() -> &'static str { diff --git a/common/gateway-requests/src/registration/handshake/shared_key/mod.rs b/common/gateway-requests/src/registration/handshake/shared_key/mod.rs new file mode 100644 index 0000000000..0c7b13f18d --- /dev/null +++ b/common/gateway-requests/src/registration/handshake/shared_key/mod.rs @@ -0,0 +1,90 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::GatewayRequestsError; +use nym_crypto::generic_array::{typenum::Unsigned, GenericArray}; +use nym_crypto::symmetric::aead::{self, AeadKey, KeySizeUser, Nonce}; +use nym_pemstore::traits::PemStorableKey; +use nym_sphinx::params::GatewayEncryptionAlgorithm; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; + +pub mod legacy; + +#[derive(Debug, PartialEq, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)] +pub struct SharedSymmetricKey(AeadKey); + +type KeySize = ::KeySize; + +#[derive(Debug, Clone, Copy, Error)] +pub enum SharedKeyConversionError { + #[error("the string representation of the shared key was malformed: {0}")] + DecodeError(#[from] bs58::decode::Error), + #[error( + "the received shared keys had invalid size. Got: {received}, but expected: {expected}" + )] + InvalidSharedKeysSize { received: usize, expected: usize }, +} + +impl SharedSymmetricKey { + pub fn try_from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != KeySize::to_usize() { + return Err(SharedKeyConversionError::InvalidSharedKeysSize { + received: bytes.len(), + expected: KeySize::to_usize(), + }); + } + + Ok(SharedSymmetricKey(GenericArray::clone_from_slice(bytes))) + } + + pub fn to_bytes(&self) -> Vec { + self.0.iter().copied().collect() + } + + pub fn try_from_base58_string>( + val: S, + ) -> Result { + let bs58_str = Zeroizing::new(val.into()); + let decoded = Zeroizing::new(bs58::decode(bs58_str).into_vec()?); + Self::try_from_bytes(&decoded) + } + + pub fn to_base58_string(&self) -> String { + let bytes = Zeroizing::new(self.to_bytes()); + bs58::encode(bytes).into_string() + } + + pub fn encrypt( + &self, + plaintext: &[u8], + nonce: &Nonce, + ) -> Result, GatewayRequestsError> { + aead::encrypt::(&self.0, &nonce, plaintext).map_err(Into::into) + } + + pub fn decrypt( + &self, + ciphertext: &[u8], + nonce: &Nonce, + ) -> Result, GatewayRequestsError> { + aead::decrypt::(&self.0, &nonce, ciphertext).map_err(Into::into) + } +} + +impl PemStorableKey for SharedSymmetricKey { + type Error = SharedKeyConversionError; + + fn pem_type() -> &'static str { + "AES-256-GCM-SIV GATEWAY SHARED KEY" + } + + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + + fn from_bytes(bytes: &[u8]) -> Result { + Self::try_from_bytes(bytes) + } +} diff --git a/common/gateway-requests/src/registration/handshake/state.rs b/common/gateway-requests/src/registration/handshake/state.rs index 70cd48620c..8a7351d22f 100644 --- a/common/gateway-requests/src/registration/handshake/state.rs +++ b/common/gateway-requests/src/registration/handshake/state.rs @@ -2,8 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::registration::handshake::error::HandshakeError; -use crate::registration::handshake::shared_key::{SharedKeySize, SharedKeys}; use crate::registration::handshake::WsItem; +use crate::registration::handshake::{LegacySharedKeySize, LegacySharedKeys}; use crate::types; use futures::{Sink, SinkExt, Stream, StreamExt}; use nym_crypto::{ @@ -12,7 +12,7 @@ use nym_crypto::{ hkdf, symmetric::stream_cipher, }; -use nym_sphinx::params::{GatewayEncryptionAlgorithm, GatewaySharedKeyHkdfAlgorithm}; +use nym_sphinx::params::{GatewaySharedKeyHkdfAlgorithm, LegacyGatewayEncryptionAlgorithm}; #[cfg(not(target_arch = "wasm32"))] use nym_task::TaskClient; use rand::{CryptoRng, RngCore}; @@ -41,7 +41,7 @@ pub(crate) struct State<'a, S> { ephemeral_keypair: encryption::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. @@ -128,12 +128,12 @@ impl<'a, S> State<'a, S> { None, &dh_result, None, - SharedKeySize::to_usize(), + LegacySharedKeySize::to_usize(), ) .expect("somehow too long okm was provided"); let derived_shared_key = - SharedKeys::try_from_bytes(&okm).expect("okm was expanded to incorrect length!"); + LegacySharedKeys::try_from_bytes(&okm).expect("okm was expanded to incorrect length!"); self.derived_shared_keys = Some(derived_shared_key) } @@ -153,8 +153,8 @@ impl<'a, S> State<'a, S> { .collect(); let signature = self.identity.private_key().sign(message); - let zero_iv = stream_cipher::zero_iv::(); - stream_cipher::encrypt::( + let zero_iv = stream_cipher::zero_iv::(); + stream_cipher::encrypt::( self.derived_shared_keys.as_ref().unwrap().encryption_key(), &zero_iv, &signature.to_bytes(), @@ -178,8 +178,8 @@ impl<'a, S> State<'a, S> { .expect("shared key was not derived!"); // first decrypt received data - let zero_iv = stream_cipher::zero_iv::(); - let decrypted_signature = stream_cipher::decrypt::( + let zero_iv = stream_cipher::zero_iv::(); + let decrypted_signature = stream_cipher::decrypt::( derived_shared_key.encryption_key(), &zero_iv, remote_material, @@ -320,7 +320,7 @@ impl<'a, S> State<'a, S> { /// Finish the handshake, yielding the derived shared key and implicitly dropping all borrowed /// values. - pub(crate) fn finalize_handshake(self) -> SharedKeys { + pub(crate) fn finalize_handshake(self) -> LegacySharedKeys { self.derived_shared_keys.unwrap() } } diff --git a/common/gateway-requests/src/types.rs b/common/gateway-requests/src/types.rs index 07145dab8d..d55ca70b72 100644 --- a/common/gateway-requests/src/types.rs +++ b/common/gateway-requests/src/types.rs @@ -4,7 +4,7 @@ use crate::authentication::encrypted_address::EncryptedAddressBytes; use crate::iv::{IVConversionError, IV}; use crate::models::CredentialSpendingRequest; -use crate::registration::handshake::SharedKeys; +use crate::registration::handshake::LegacySharedKeys; use crate::{GatewayMacSize, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION}; use nym_credentials::ecash::bandwidth::CredentialSpendingData; use nym_credentials_interface::CompactEcashError; @@ -14,11 +14,12 @@ use nym_crypto::symmetric::stream_cipher; use nym_sphinx::addressing::nodes::NymNodeRoutingAddressError; use nym_sphinx::forwarding::packet::{MixPacket, MixPacketFormattingError}; use nym_sphinx::params::packet_sizes::PacketSize; -use nym_sphinx::params::{GatewayEncryptionAlgorithm, GatewayIntegrityHmacAlgorithm}; +use nym_sphinx::params::{GatewayIntegrityHmacAlgorithm, LegacyGatewayEncryptionAlgorithm}; use nym_sphinx::DestinationAddressBytes; use serde::{Deserialize, Serialize}; use tracing::log::error; +use nym_crypto::symmetric::aead::AeadError; use std::str::FromStr; use std::string::FromUtf8Error; use thiserror::Error; @@ -154,6 +155,9 @@ pub enum GatewayRequestsError { #[error("the provided [v1] credential has invalid number of parameters - {0}")] InvalidNumberOfEmbededParameters(u32), + #[error("failed to either encrypt or decrypt provided message")] + AeadFailure(#[from] AeadError), + // variant to catch legacy errors #[error("{0}")] Other(String), @@ -236,7 +240,7 @@ impl ClientControlRequest { pub fn new_enc_ecash_credential( credential: CredentialSpendingData, - shared_key: &SharedKeys, + shared_key: &LegacySharedKeys, iv: IV, ) -> Self { let cred = CredentialSpendingRequest::new(credential); @@ -251,7 +255,7 @@ impl ClientControlRequest { pub fn try_from_enc_ecash_credential( enc_credential: Vec, - shared_key: &SharedKeys, + shared_key: &LegacySharedKeys, iv: Vec, ) -> Result { let iv = IV::try_from_bytes(&iv)?; @@ -388,7 +392,7 @@ pub enum BinaryRequest { impl BinaryRequest { pub fn try_from_encrypted_tagged_bytes( raw_req: Vec, - shared_keys: &SharedKeys, + shared_keys: &LegacySharedKeys, ) -> Result { let message_bytes = &shared_keys.decrypt_tagged(&raw_req, None)?; @@ -398,7 +402,7 @@ impl BinaryRequest { Ok(BinaryRequest::ForwardSphinx(mix_packet)) } - pub fn into_encrypted_tagged_bytes(self, shared_key: &SharedKeys) -> Vec { + pub fn into_encrypted_tagged_bytes(self, shared_key: &LegacySharedKeys) -> Vec { match self { BinaryRequest::ForwardSphinx(mix_packet) => { let forwarding_data = match mix_packet.into_bytes() { @@ -421,7 +425,7 @@ impl BinaryRequest { BinaryRequest::ForwardSphinx(mix_packet) } - pub fn into_ws_message(self, shared_key: &SharedKeys) -> Message { + pub fn into_ws_message(self, shared_key: &LegacySharedKeys) -> Message { Message::Binary(self.into_encrypted_tagged_bytes(shared_key)) } } @@ -434,7 +438,7 @@ pub enum BinaryResponse { impl BinaryResponse { pub fn try_from_encrypted_tagged_bytes( raw_req: Vec, - shared_keys: &SharedKeys, + shared_keys: &LegacySharedKeys, ) -> Result { let mac_size = GatewayMacSize::to_usize(); if raw_req.len() < mac_size { @@ -452,8 +456,8 @@ impl BinaryResponse { return Err(GatewayRequestsError::InvalidMac); } - let zero_iv = stream_cipher::zero_iv::(); - let plaintext = stream_cipher::decrypt::( + let zero_iv = stream_cipher::zero_iv::(); + let plaintext = stream_cipher::decrypt::( shared_keys.encryption_key(), &zero_iv, message_bytes, @@ -462,7 +466,7 @@ impl BinaryResponse { Ok(BinaryResponse::PushedMixMessage(plaintext)) } - pub fn into_encrypted_tagged_bytes(self, shared_key: &SharedKeys) -> Vec { + pub fn into_encrypted_tagged_bytes(self, shared_key: &LegacySharedKeys) -> Vec { match self { // TODO: it could be theoretically slightly more efficient if the data wasn't taken // by reference because then it makes a copy for encryption rather than do it in place @@ -474,7 +478,7 @@ impl BinaryResponse { BinaryResponse::PushedMixMessage(msg) } - pub fn into_ws_message(self, shared_key: &SharedKeys) -> Message { + pub fn into_ws_message(self, shared_key: &LegacySharedKeys) -> Message { Message::Binary(self.into_encrypted_tagged_bytes(shared_key)) } } diff --git a/common/gateway-storage/src/lib.rs b/common/gateway-storage/src/lib.rs index 0cc49f5478..24fd0ee7b1 100644 --- a/common/gateway-storage/src/lib.rs +++ b/common/gateway-storage/src/lib.rs @@ -11,7 +11,7 @@ use models::{ VerifiedTicket, WireguardPeer, }; use nym_credentials_interface::ClientTicket; -use nym_gateway_requests::registration::handshake::SharedKeys; +use nym_gateway_requests::registration::handshake::LegacySharedKeys; use nym_sphinx::DestinationAddressBytes; use shared_keys::SharedKeysManager; use sqlx::ConnectOptions; @@ -46,7 +46,7 @@ pub trait Storage: Send + Sync { async fn insert_shared_keys( &self, client_address: DestinationAddressBytes, - shared_keys: &SharedKeys, + shared_keys: &LegacySharedKeys, ) -> Result; /// Tries to retrieve shared keys stored for the particular client. @@ -330,7 +330,7 @@ impl Storage for PersistentStorage { async fn insert_shared_keys( &self, client_address: DestinationAddressBytes, - shared_keys: &SharedKeys, + shared_keys: &LegacySharedKeys, ) -> Result { let client_id = self .client_manager diff --git a/common/nymsphinx/acknowledgements/Cargo.toml b/common/nymsphinx/acknowledgements/Cargo.toml index 8092813cea..117bc6b7d5 100644 --- a/common/nymsphinx/acknowledgements/Cargo.toml +++ b/common/nymsphinx/acknowledgements/Cargo.toml @@ -14,7 +14,7 @@ generic-array = { workspace = true, optional = true, features = ["serde"] } thiserror = { workspace = true } zeroize = { workspace = true } -nym-crypto = { path = "../../crypto", features = ["symmetric", "rand"] } +nym-crypto = { path = "../../crypto", features = ["stream_cipher", "rand"] } nym-pemstore = { path = "../../pemstore" } nym-sphinx-addressing = { path = "../addressing" } nym-sphinx-params = { path = "../params" } diff --git a/common/nymsphinx/anonymous-replies/Cargo.toml b/common/nymsphinx/anonymous-replies/Cargo.toml index f8a1174d62..84c21d0bd9 100644 --- a/common/nymsphinx/anonymous-replies/Cargo.toml +++ b/common/nymsphinx/anonymous-replies/Cargo.toml @@ -13,7 +13,7 @@ bs58 = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } -nym-crypto = { path = "../../crypto", features = ["symmetric", "rand"] } +nym-crypto = { path = "../../crypto", features = ["stream_cipher", "rand"] } nym-sphinx-addressing = { path = "../addressing" } nym-sphinx-params = { path = "../params" } nym-sphinx-routing = { path = "../routing" } diff --git a/common/nymsphinx/params/Cargo.toml b/common/nymsphinx/params/Cargo.toml index 7933943630..bf477580f4 100644 --- a/common/nymsphinx/params/Cargo.toml +++ b/common/nymsphinx/params/Cargo.toml @@ -11,7 +11,7 @@ repository = { workspace = true } thiserror = { workspace = true } serde = { workspace = true, features = ["derive"] } -nym-crypto = { path = "../../crypto", features = ["hashing", "symmetric"] } +nym-crypto = { path = "../../crypto", features = ["hashing", "stream_cipher", "aes-gcm-siv"] } nym-sphinx-types = { path = "../types" } [features] diff --git a/common/nymsphinx/params/src/lib.rs b/common/nymsphinx/params/src/lib.rs index 7ad247dfa5..9d899a426b 100644 --- a/common/nymsphinx/params/src/lib.rs +++ b/common/nymsphinx/params/src/lib.rs @@ -4,6 +4,7 @@ use nym_crypto::aes::Aes128; use nym_crypto::blake3; use nym_crypto::ctr; +use nym_crypto::Aes256GcmSiv; type Aes128Ctr = ctr::Ctr64BE; @@ -48,7 +49,7 @@ pub type GatewaySharedKeyHkdfAlgorithm = blake3::Hasher; pub type ReplySurbKeyDigestAlgorithm = blake3::Hasher; /// Hashing algorithm used when computing integrity (H)Mac for message exchanged between client and gateway. -// TODO: if updated, the pem type defined in gateway\gateway-requests\src\registration\handshake\shared_key +// TODO: if updated, the pem type defined in gateway\gateway-requests\src\registration\handshake\legacy_shared_key // needs updating! pub type GatewayIntegrityHmacAlgorithm = blake3::Hasher; @@ -59,11 +60,16 @@ pub type GatewayIntegrityHmacAlgorithm = blake3::Hasher; // - the pem type defined in nym\common\nymsphinx\acknowledgements\src\key needs updating! pub type AckEncryptionAlgorithm = Aes128Ctr; -/// Encryption algorithm used for end-to-end encryption of messages exchanged between clients +/// Legacy encryption algorithm used for end-to-end encryption of messages exchanged between clients /// and their gateways. -// TODO: if updated, the pem type defined in gateway\gateway-requests\src\registration\handshake\shared_key +// TODO: if updated, the pem type defined in gateway\gateway-requests\src\registration\handshake\legacy_shared_key // needs updating! -pub type GatewayEncryptionAlgorithm = Aes128Ctr; +pub type LegacyGatewayEncryptionAlgorithm = Aes128Ctr; + +/// Encryption algorithm used for end-to-end encryption of messages exchanged between clients +/// and their gateways. +// NOTE: if updated, the pem type defined in gateway\gateway-requests\src\registration\handshake\shared_key +pub type GatewayEncryptionAlgorithm = Aes256GcmSiv; /// Encryption algorithm used for end-to-end encryption of messages exchanged between clients that are /// encapsulated inside sphinx packets. 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 a4beeb1cc6..a2337c8955 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -21,10 +21,11 @@ use nym_crypto::asymmetric::identity; use nym_gateway_requests::authentication::encrypted_address::{ EncryptedAddressBytes, EncryptedAddressConversionError, }; -use nym_gateway_requests::registration::handshake::shared_key::SharedKeyConversionError; use nym_gateway_requests::{ iv::{IVConversionError, IV}, - registration::handshake::{error::HandshakeError, gateway_handshake, SharedKeys}, + registration::handshake::{ + error::HandshakeError, gateway_handshake, LegacySharedKeys, SharedKeyConversionError, + }, types::{ClientControlRequest, ServerResponse}, BinaryResponse, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, }; @@ -177,7 +178,7 @@ where async fn perform_registration_handshake( &mut self, init_msg: Vec, - ) -> Result + ) -> Result where S: AsyncRead + AsyncWrite + Unpin + Send, { @@ -258,7 +259,7 @@ where /// * `packets`: unwrapped packets that are to be pushed back to the client. pub(crate) async fn push_packets_to_client( &mut self, - shared_keys: &SharedKeys, + shared_keys: &LegacySharedKeys, packets: Vec>, ) -> Result<(), WsError> where @@ -313,7 +314,7 @@ where async fn push_stored_messages_to_client( &mut self, client_address: DestinationAddressBytes, - shared_keys: &SharedKeys, + shared_keys: &LegacySharedKeys, ) -> Result<(), InitialAuthenticationError> where S: AsyncRead + AsyncWrite + Unpin, @@ -367,7 +368,7 @@ where client_address: DestinationAddressBytes, encrypted_address: EncryptedAddressBytes, iv: IV, - ) -> Result, InitialAuthenticationError> { + ) -> Result, InitialAuthenticationError> { let shared_keys = self .shared_state .storage @@ -378,7 +379,7 @@ where // this should never fail as we only ever construct persisted shared keys ourselves when inserting // data to the storage. The only way it could fail is if we somehow changed implementation without // performing proper migration - let keys = SharedKeys::try_from_base58_string( + let keys = LegacySharedKeys::try_from_base58_string( shared_keys.derived_aes128_ctr_blake3_hmac_keys_bs58, ) .map_err(|source| { @@ -451,7 +452,7 @@ where client_address: DestinationAddressBytes, encrypted_address: EncryptedAddressBytes, iv: IV, - ) -> Result, InitialAuthenticationError> + ) -> Result, InitialAuthenticationError> where S: AsyncRead + AsyncWrite + Unpin, { @@ -616,7 +617,7 @@ where async fn register_client( &mut self, client_address: DestinationAddressBytes, - client_shared_keys: &SharedKeys, + client_shared_keys: &LegacySharedKeys, ) -> Result where S: AsyncRead + AsyncWrite + Unpin, 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 6251ab86bb..fa78d8fe8b 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::registration::handshake::SharedKeys; +use nym_gateway_requests::registration::handshake::LegacySharedKeys; use nym_gateway_requests::ServerResponse; use nym_gateway_storage::Storage; use nym_sphinx::DestinationAddressBytes; @@ -46,11 +46,15 @@ pub(crate) struct ClientDetails { #[zeroize(skip)] pub(crate) address: DestinationAddressBytes, pub(crate) id: i64, - pub(crate) shared_keys: SharedKeys, + pub(crate) shared_keys: LegacySharedKeys, } impl ClientDetails { - pub(crate) fn new(id: i64, address: DestinationAddressBytes, shared_keys: SharedKeys) -> Self { + pub(crate) fn new( + id: i64, + address: DestinationAddressBytes, + shared_keys: LegacySharedKeys, + ) -> Self { ClientDetails { address, id, From 71532484a944e731d813583b001cd4a189db79fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 13 Sep 2024 18:02:30 +0100 Subject: [PATCH 02/17] updated client handshake to allow derivation of different key types --- Cargo.lock | 2 +- common/client-libs/gateway-client/Cargo.toml | 2 +- .../gateway-client/src/bandwidth.rs | 4 +- .../gateway-client/src/client/mod.rs | 68 ++++-- common/client-libs/gateway-client/src/lib.rs | 2 +- .../gateway-client/src/packet_router.rs | 4 +- .../gateway-client/src/socket_state.rs | 4 +- .../client-libs/gateway-client/src/traits.rs | 2 +- common/crypto/src/symmetric/aead.rs | 8 + common/crypto/src/symmetric/stream_cipher.rs | 14 +- .../src/registration/handshake/client.rs | 159 ++++--------- .../src/registration/handshake/error.rs | 18 +- .../src/registration/handshake/gateway.rs | 151 ++++--------- .../src/registration/handshake/messages.rs | 213 ++++++++++++++++++ .../src/registration/handshake/mod.rs | 52 +++-- .../handshake/shared_key/legacy.rs | 10 +- .../registration/handshake/shared_key/mod.rs | 106 ++++++++- .../src/registration/handshake/state.rs | 210 ++++++++++------- common/gateway-requests/src/types.rs | 24 +- common/nymsphinx/params/Cargo.toml | 2 +- .../websocket/connection_handler/fresh.rs | 3 +- 21 files changed, 672 insertions(+), 386 deletions(-) create mode 100644 common/gateway-requests/src/registration/handshake/messages.rs diff --git a/Cargo.lock b/Cargo.lock index 8395d243d6..c519976376 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4972,7 +4972,6 @@ dependencies = [ "futures", "getrandom", "gloo-utils 0.2.0", - "log", "nym-bandwidth-controller", "nym-credential-storage", "nym-credentials", @@ -4991,6 +4990,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-tungstenite", + "tracing", "tungstenite 0.20.1", "url", "wasm-bindgen", diff --git a/common/client-libs/gateway-client/Cargo.toml b/common/client-libs/gateway-client/Cargo.toml index 6390e3a844..dc8bfad577 100644 --- a/common/client-libs/gateway-client/Cargo.toml +++ b/common/client-libs/gateway-client/Cargo.toml @@ -11,7 +11,7 @@ license.workspace = true # TODO: (for this and other crates), similarly to 'tokio', import only required "futures" modules rather than # the entire crate futures = { workspace = true } -log = { workspace = true } +tracing = { workspace = true } thiserror = { workspace = true } url = { workspace = true } rand = { workspace = true } diff --git a/common/client-libs/gateway-client/src/bandwidth.rs b/common/client-libs/gateway-client/src/bandwidth.rs index f232c004ec..50641f9894 100644 --- a/common/client-libs/gateway-client/src/bandwidth.rs +++ b/common/client-libs/gateway-client/src/bandwidth.rs @@ -53,9 +53,9 @@ impl ClientBandwidth { let remaining_bi2 = bibytes2(remaining as f64); if remaining < 0 { - log::warn!("OUT OF BANDWIDTH. remaining: {remaining_bi2}"); + tracing::warn!("OUT OF BANDWIDTH. remaining: {remaining_bi2}"); } else { - log::info!("remaining bandwidth: {remaining_bi2}"); + tracing::info!("remaining bandwidth: {remaining_bi2}"); } self.inner diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index 6f29ea5b0b..40dd11d670 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -1,5 +1,6 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 + use crate::bandwidth::ClientBandwidth; use crate::client::config::GatewayClientConfig; use crate::error::GatewayClientError; @@ -11,7 +12,6 @@ use crate::socket_state::{ws_fd, PartiallyDelegatedHandle, SocketState}; use crate::traits::GatewayPacketRouter; use crate::{cleanup_socket_message, try_decrypt_binary_message}; use futures::{SinkExt, StreamExt}; -use log::*; use nym_bandwidth_controller::{BandwidthController, BandwidthStatusMessage}; use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage; use nym_credential_storage::storage::Storage as CredentialStorage; @@ -19,16 +19,19 @@ use nym_credentials::CredentialSpendingData; use nym_crypto::asymmetric::identity; use nym_gateway_requests::authentication::encrypted_address::EncryptedAddressBytes; use nym_gateway_requests::iv::IV; -use nym_gateway_requests::registration::handshake::{client_handshake, LegacySharedKeys}; +use nym_gateway_requests::registration::handshake::{ + client_handshake, LegacySharedKeys, SharedGatewayKey, +}; use nym_gateway_requests::{ - BinaryRequest, ClientControlRequest, ServerResponse, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, - CURRENT_PROTOCOL_VERSION, + BinaryRequest, ClientControlRequest, ServerResponse, AES_GCM_SIV_PROTOCOL_VERSION, + CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION, }; use nym_sphinx::forwarding::packet::MixPacket; use nym_task::TaskClient; use nym_validator_client::nyxd::contract_traits::DkgQueryClient; use rand::rngs::OsRng; use std::sync::Arc; +use tracing::*; use tungstenite::protocol::Message; use url::Url; @@ -41,6 +44,8 @@ use tokio_tungstenite::connect_async; #[cfg(not(unix))] use std::os::raw::c_int as RawFd; +use std::time::Duration; +use tracing::instrument; #[cfg(target_arch = "wasm32")] use wasm_utils::websocket::JSWebsocket; #[cfg(target_arch = "wasm32")] @@ -398,13 +403,13 @@ impl GatewayClient { } } - async fn register(&mut self) -> Result<(), GatewayClientError> { + async fn register(&mut self, legacy: bool) -> Result<(), GatewayClientError> { if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } debug_assert!(self.connection.is_available()); - log::debug!("Registering gateway"); + log::debug!("registering with gateway. using legacy key derivation: {legacy}"); // 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 @@ -422,8 +427,11 @@ impl GatewayClient { ) .await .map_err(GatewayClientError::RegistrationFailure), - _ => unreachable!(), - }?; + _ => return Err(GatewayClientError::ConnectionInInvalidState), + }; + + println!("registration result: {shared_key:?}"); + let (authentication_status, gateway_protocol) = match self.read_control_response().await? { ServerResponse::Register { protocol_version, @@ -438,19 +446,21 @@ impl GatewayClient { self.check_gateway_protocol(gateway_protocol)?; self.authenticated = authentication_status; - if self.authenticated { - self.shared_key = Some(Arc::new(shared_key)); - } - - // populate the negotiated protocol for future uses - self.negotiated_protocol = gateway_protocol; - - Ok(()) + todo!() + // if self.authenticated { + // self.shared_key = Some(Arc::new(shared_key)); + // } + // + // // populate the negotiated protocol for future uses + // self.negotiated_protocol = gateway_protocol; + // + // Ok(()) } async fn authenticate( &mut self, shared_key: Option, + supports_aes_gcm_siv: bool, ) -> Result<(), GatewayClientError> { if shared_key.is_none() && self.shared_key.is_none() { return Err(GatewayClientError::NoSharedKeyAvailable); @@ -460,6 +470,8 @@ impl GatewayClient { } log::debug!("Authenticating with gateway"); + todo!("if using legacy key, check if we can upgrade"); + // 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 let mut rng = OsRng; @@ -506,10 +518,30 @@ impl GatewayClient { } /// 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 + ) + )] pub async fn perform_initial_authentication( &mut self, ) -> Result, GatewayClientError> { // 1. check gateway's protocol version + let supports_aes_gcm_siv = match self.get_gateway_protocol().await { + Ok(protocol) => protocol >= AES_GCM_SIV_PROTOCOL_VERSION, + 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?; + false + } + }; + + if !supports_aes_gcm_siv { + warn!("this gateway is on an old version that doesn't support AES256-GCM-SIV"); + } + // 2. if error or new handshake unsupported => fallback to the old registration/authentication // 3. otherwise continue with the updated key derivation @@ -525,9 +557,9 @@ impl GatewayClient { } if self.shared_key.is_some() { - self.authenticate(None).await?; + self.authenticate(None, supports_aes_gcm_siv).await?; } else { - self.register().await?; + self.register(!supports_aes_gcm_siv).await?; } if self.authenticated { // if we are authenticated it means we MUST have an associated shared_key diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index a07e03a258..319c98b23f 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -2,8 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::GatewayClientError; -use log::warn; use nym_gateway_requests::BinaryResponse; +use tracing::warn; use tungstenite::{protocol::Message, Error as WsError}; pub use client::{config::GatewayClientConfig, GatewayClient, GatewayConfig}; diff --git a/common/client-libs/gateway-client/src/packet_router.rs b/common/client-libs/gateway-client/src/packet_router.rs index ab94ecbac3..36168f4ab8 100644 --- a/common/client-libs/gateway-client/src/packet_router.rs +++ b/common/client-libs/gateway-client/src/packet_router.rs @@ -44,7 +44,7 @@ impl PacketRouter { // having already been dropped if self.shutdown.is_shutdown_poll() || self.shutdown.is_dummy() { // This should ideally not happen, but it's ok - log::warn!("Failed to send mixnet messages due to receiver task shutdown"); + tracing::warn!("Failed to send mixnet messages due to receiver task shutdown"); return Err(GatewayClientError::ShutdownInProgress); } // This should never happen during ordinary operation the way it's currently used. @@ -60,7 +60,7 @@ impl PacketRouter { // having already been dropped if self.shutdown.is_shutdown_poll() || self.shutdown.is_dummy() { // This should ideally not happen, but it's ok - log::warn!("Failed to send acks due to receiver task shutdown"); + tracing::warn!("Failed to send acks due to receiver task shutdown"); return Err(GatewayClientError::ShutdownInProgress); } // This should never happen during ordinary operation the way it's currently used. diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index d8a7e7c747..46d81cb895 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -9,15 +9,15 @@ use crate::{cleanup_socket_messages, try_decrypt_binary_message}; use futures::channel::oneshot; use futures::stream::{SplitSink, SplitStream}; use futures::{SinkExt, StreamExt}; -use log::*; use nym_gateway_requests::registration::handshake::LegacySharedKeys; use nym_gateway_requests::{ServerResponse, SimpleGatewayRequestsError}; use nym_task::TaskClient; +use si_scale::helpers::bibytes2; use std::os::raw::c_int as RawFd; use std::sync::Arc; +use tracing::*; use tungstenite::{protocol::Message, Error as WsError}; -use si_scale::helpers::bibytes2; #[cfg(unix)] use std::os::fd::AsRawFd; #[cfg(not(target_arch = "wasm32"))] diff --git a/common/client-libs/gateway-client/src/traits.rs b/common/client-libs/gateway-client/src/traits.rs index cb0a3f4d22..df6b458ba8 100644 --- a/common/client-libs/gateway-client/src/traits.rs +++ b/common/client-libs/gateway-client/src/traits.rs @@ -1,9 +1,9 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use log::{error, trace, warn}; use nym_sphinx::addressing::nodes::MAX_NODE_ADDRESS_UNPADDED_LEN; use nym_sphinx::params::PacketSize; +use tracing::{error, trace, warn}; pub trait GatewayPacketRouter { type Error: std::error::Error; diff --git a/common/crypto/src/symmetric/aead.rs b/common/crypto/src/symmetric/aead.rs index 6081d4c9c2..2ac4716815 100644 --- a/common/crypto/src/symmetric/aead.rs +++ b/common/crypto/src/symmetric/aead.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use aead::{Aead, AeadCore, AeadInPlace, Buffer, KeyInit, Payload}; +use generic_array::typenum::Unsigned; #[cfg(feature = "rand")] use rand::{CryptoRng, RngCore}; @@ -28,6 +29,13 @@ where ::generate_nonce(rng) } +pub fn nonce_size() -> usize +where + A: AeadCore, +{ + <::NonceSize>::to_usize() +} + #[inline] pub fn encrypt<'msg, 'aad, A>( key: &AeadKey, diff --git a/common/crypto/src/symmetric/stream_cipher.rs b/common/crypto/src/symmetric/stream_cipher.rs index c59843050b..4bd21eb031 100644 --- a/common/crypto/src/symmetric/stream_cipher.rs +++ b/common/crypto/src/symmetric/stream_cipher.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use cipher::{Iv, StreamCipher}; +use generic_array::typenum::Unsigned; #[cfg(feature = "rand")] use rand::{CryptoRng, RngCore}; @@ -37,7 +38,7 @@ where #[cfg(feature = "rand")] pub fn random_iv(rng: &mut R) -> IV where - C: KeyIvInit, + C: IvSizeUser, R: RngCore + CryptoRng, { let mut iv = IV::::default(); @@ -45,16 +46,23 @@ where iv } +pub fn iv_size() -> usize +where + C: IvSizeUser, +{ + <::IvSize>::to_usize() +} + pub fn zero_iv() -> IV where - C: KeyIvInit, + C: IvSizeUser, { Iv::::default() } pub fn iv_from_slice(b: &[u8]) -> &IV where - C: KeyIvInit, + C: IvSizeUser, { if b.len() != C::iv_size() { // `from_slice` would have caused a panic about this issue anyway. diff --git a/common/gateway-requests/src/registration/handshake/client.rs b/common/gateway-requests/src/registration/handshake/client.rs index fe099e6178..aebda07551 100644 --- a/common/gateway-requests/src/registration/handshake/client.rs +++ b/common/gateway-requests/src/registration/handshake/client.rs @@ -1,129 +1,56 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::registration::handshake::messages::{Finalization, GatewayMaterialExchange}; use crate::registration::handshake::state::State; -use crate::registration::handshake::LegacySharedKeys; +use crate::registration::handshake::SharedGatewayKey; use crate::registration::handshake::{error::HandshakeError, WsItem}; -use futures::future::BoxFuture; -use futures::task::{Context, Poll}; -use futures::{Future, Sink, Stream}; -use nym_crypto::asymmetric::encryption::PUBLIC_KEY_SIZE; -use nym_crypto::asymmetric::identity::SIGNATURE_LENGTH; -use nym_crypto::asymmetric::{encryption, identity}; -use rand::{CryptoRng, RngCore}; -use std::pin::Pin; +use futures::{Sink, Stream}; use tungstenite::Message as WsMessage; -pub(crate) struct ClientHandshake<'a> { - handshake_future: BoxFuture<'a, Result>, -} - -impl<'a> ClientHandshake<'a> { - pub(crate) fn new( - rng: &mut (impl RngCore + CryptoRng), - ws_stream: &'a mut S, - identity: &'a nym_crypto::asymmetric::identity::KeyPair, - gateway_pubkey: identity::PublicKey, - expects_credential_usage: bool, - #[cfg(not(target_arch = "wasm32"))] shutdown: nym_task::TaskClient, - ) -> Self +impl<'a, S> State<'a, S> { + async fn client_handshake_inner(&mut self) -> Result<(), HandshakeError> where - S: Stream + Sink + Unpin + Send + 'a, + S: Stream + Sink + Unpin, { - let mut state = State::new( - rng, - ws_stream, - identity, - Some(gateway_pubkey), - expects_credential_usage, - #[cfg(not(target_arch = "wasm32"))] - shutdown, - ); - - ClientHandshake { - handshake_future: Box::pin(async move { - // If any step along the way failed (that are non-network related), - // try to send 'error' message to the remote - // party to indicate handshake should be terminated - pub(crate) async fn check_processing_error( - result: Result, - state: &mut State<'_, S>, - ) -> Result - where - S: Sink + Unpin, - { - match result { - Ok(ok) => Ok(ok), - Err(err) => { - state.send_handshake_error(err.to_string()).await?; - Err(err) - } - } - } - - let init_message = state.init_message(); - state.send_handshake_data(init_message).await?; - - // <- g^y || AES(k, sig(gate_priv, (g^y || g^x)) - let mid_res = state.receive_handshake_message().await?; - let (remote_ephemeral_key, remote_key_material) = - check_processing_error(Self::parse_mid_response(mid_res), &mut state).await?; - - // hkdf::::(g^xy) - state.derive_shared_key(&remote_ephemeral_key); - let verification_res = - state.verify_remote_key_material(&remote_key_material, &remote_ephemeral_key); - check_processing_error(verification_res, &mut state).await?; - - // AES(k, sig(client_priv, (g^y || g^x)) - let material = state.prepare_key_material_sig(&remote_ephemeral_key); - - // -> AES(k, sig(client_priv, g^x || g^y)) - state.send_handshake_data(material).await?; - - // <- Ok - let finalization = state.receive_handshake_message().await?; - check_processing_error(Self::parse_finalization_response(finalization), &mut state) - .await?; - Ok(state.finalize_handshake()) - }), - } + // 1. send ed25519 pubkey alongside ephemeral x25519 pubkey and a flag to indicate non-legacy client + // LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_NON_LEGACY + let init_message = self.init_message(); + self.send_handshake_data(init_message).await?; + + // 2. wait for response with remote x25519 pubkey as well as encrypted signature + // <- g^y || AES(k, sig(gate_priv, (g^y || g^x)) || MAYBE_NONCE + let mid_res = self + .receive_handshake_message::() + .await?; + + // 3. derive shared keys locally + // hkdf::::(g^xy) + self.derive_shared_key(&mid_res.ephemeral_dh); + + // 4. verify the received signature using the locally derived keys + self.verify_remote_key_material(&mid_res.materials, &mid_res.ephemeral_dh)?; + + // 5. produce our own materials to get verified by the remote + // -> AES(k, sig(client_priv, g^x || g^y)) || MAYBE_NONCE + let materials = self.prepare_key_material_sig(&mid_res.ephemeral_dh)?; + self.send_handshake_data(materials).await?; + + // 6. wait for remote confirmation of finalizing the handshake + let finalization = self.receive_handshake_message::().await?; + finalization.ensure_success()?; + Ok(()) } - // client should have received - // G^y || AES(k, SIG(PRIV_GATE, G^y || G^x)) - fn parse_mid_response( - mut resp: Vec, - ) -> Result<(encryption::PublicKey, Vec), HandshakeError> { - if resp.len() != PUBLIC_KEY_SIZE + SIGNATURE_LENGTH { - return Err(HandshakeError::MalformedResponse); - } - - let remote_key_material = resp.split_off(PUBLIC_KEY_SIZE); - // this can only fail if the provided bytes have len different from PUBLIC_KEY_SIZE - // which is impossible - let remote_ephemeral_key = encryption::PublicKey::from_bytes(&resp).unwrap(); - Ok((remote_ephemeral_key, remote_key_material)) - } - - fn parse_finalization_response(resp: Vec) -> Result<(), HandshakeError> { - if resp.len() != 1 { - return Err(HandshakeError::MalformedResponse); - } - if resp[0] == 1 { - Ok(()) - } else if resp[0] == 0 { - Err(HandshakeError::HandshakeFailure) - } else { - Err(HandshakeError::MalformedResponse) - } - } -} - -impl<'a> Future for ClientHandshake<'a> { - type Output = Result; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - Pin::new(&mut self.handshake_future).poll(cx) + pub(crate) async fn perform_client_handshake( + mut self, + ) -> Result + where + S: Stream + Sink + Unpin, + { + let handshake_res = self.client_handshake_inner().await; + self.check_for_handshake_processing_error(handshake_res) + .await?; + Ok(self.finalize_handshake()) } } diff --git a/common/gateway-requests/src/registration/handshake/error.rs b/common/gateway-requests/src/registration/handshake/error.rs index 6e82ef040c..f78ec07af5 100644 --- a/common/gateway-requests/src/registration/handshake/error.rs +++ b/common/gateway-requests/src/registration/handshake/error.rs @@ -1,16 +1,20 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_crypto::asymmetric::identity; +use crate::registration::handshake::shared_key::SharedKeyUsageError; use thiserror::Error; -#[derive(Debug, Clone, Error)] +#[derive(Debug, Error)] pub enum HandshakeError { - #[error( - "received key material of invalid length - {0}. Expected: {}", - identity::SIGNATURE_LENGTH - )] - KeyMaterialOfInvalidSize(usize), + #[error("received key material of invalid length: {received}. Expected: {expected}")] + KeyMaterialOfInvalidSize { received: usize, expected: usize }, + + #[error("no nonce has been provided for aes256-gcm-siv key derivation")] + MissingNonceForCurrentKey, + + #[error(transparent)] + KeyUsageFailure(#[from] SharedKeyUsageError), + #[error("received invalid signature")] InvalidSignature, #[error("encountered network error")] diff --git a/common/gateway-requests/src/registration/handshake/gateway.rs b/common/gateway-requests/src/registration/handshake/gateway.rs index 52fe886263..877563ed53 100644 --- a/common/gateway-requests/src/registration/handshake/gateway.rs +++ b/common/gateway-requests/src/registration/handshake/gateway.rs @@ -1,114 +1,63 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::registration::handshake::messages::{ + HandshakeMessage, Initialisation, MaterialExchange, +}; use crate::registration::handshake::state::State; -use crate::registration::handshake::LegacySharedKeys; +use crate::registration::handshake::SharedGatewayKey; use crate::registration::handshake::{error::HandshakeError, WsItem}; -use futures::future::BoxFuture; -use futures::task::{Context, Poll}; -use futures::{Future, Sink, Stream}; -use nym_crypto::asymmetric::encryption; -use nym_task::TaskClient; -use rand::{CryptoRng, RngCore}; -use std::pin::Pin; +use futures::{Sink, Stream}; use tungstenite::Message as WsMessage; -pub(crate) struct GatewayHandshake<'a> { - handshake_future: BoxFuture<'a, Result>, -} - -impl<'a> GatewayHandshake<'a> { - pub(crate) fn new( - rng: &mut (impl RngCore + CryptoRng), - ws_stream: &'a mut S, - identity: &'a nym_crypto::asymmetric::identity::KeyPair, - received_init_payload: Vec, - shutdown: TaskClient, - ) -> Self +impl<'a, S> State<'a, S> { + async fn gateway_handshake_inner( + &mut self, + raw_init_message: Vec, + ) -> Result<(), HandshakeError> where - S: Stream + Sink + Unpin + Send + 'a, + S: Stream + Sink + Unpin, { - let mut state = State::new(rng, ws_stream, identity, None, true, shutdown); - GatewayHandshake { - handshake_future: Box::pin(async move { - // If any step along the way failed (that are non-network related), - // try to send 'error' message to the remote - // party to indicate handshake should be terminated - pub(crate) async fn check_processing_error( - result: Result, - state: &mut State<'_, S>, - ) -> Result - where - S: Sink + Unpin, - { - match result { - Ok(ok) => Ok(ok), - Err(err) => { - state.send_handshake_error(err.to_string()).await?; - Err(err) - } - } - } - - // init: <- pub_key || g^x - let (remote_identity, remote_ephemeral_key) = check_processing_error( - State::::parse_init_message(received_init_payload), - &mut state, - ) - .await?; - state.update_remote_identity(remote_identity); - - // hkdf::::(g^xy) - state.derive_shared_key(&remote_ephemeral_key); - - // AES(k, sig(gate_priv, (g^y || g^x)) - let material = state.prepare_key_material_sig(&remote_ephemeral_key); - - // g^y || AES(k, sig(gate_priv, (g^y || g^x)) - let handshake_payload = Self::combine_material_with_ephemeral_key( - state.local_ephemeral_key(), - material, - ); - - // -> g^y || AES(k, sig(gate_priv, (g^y || g^x)) - state.send_handshake_data(handshake_payload).await?; - - // <- AES(k, sig(client_priv, g^x || g^y)) - let remote_key_material = state.receive_handshake_message().await?; - let verification_res = - state.verify_remote_key_material(&remote_key_material, &remote_ephemeral_key); - check_processing_error(verification_res, &mut state).await?; - let finalizer = Self::prepare_finalization_response(); - - // -> Ok - state.send_handshake_data(finalizer).await?; - Ok(state.finalize_handshake()) - }), - } + // 1. receive remote ed25519 pubkey alongside ephemeral x25519 pubkey and maybe a flag indicating non-legacy client + // LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_NON_LEGACY + let init_message = Initialisation::try_from_bytes(&raw_init_message)?; + self.update_remote_identity(init_message.identity); + self.set_aes256_gcm_siv_key_derivation(init_message.derive_aes256_gcm_siv_key); + + // 2. derive shared keys locally + // hkdf::::(g^xy) + self.derive_shared_key(&init_message.ephemeral_dh); + + // 3. send ephemeral x25519 pubkey alongside the encrypted signature + // g^y || AES(k, sig(gate_priv, (g^y || g^x)) + let material = self + .prepare_key_material_sig(&init_message.ephemeral_dh)? + .attach_ephemeral_dh(*self.local_ephemeral_key()); + self.send_handshake_data(material).await?; + + // 4. wait for the remote response with their own encrypted signature + let materials = self.receive_handshake_message::().await?; + + // 5. verify the received signature using the locally derived keys + self.verify_remote_key_material(&materials, &init_message.ephemeral_dh)?; + + // 6. finally send the finalization message to conclude the exchange + let finalizer = self.finalization_message(); + self.send_handshake_data(finalizer).await?; + + Ok(()) } - // create g^y || AES(k, sig(gate_priv, (g^y || g^x)) - fn combine_material_with_ephemeral_key( - ephemeral_key: &encryption::PublicKey, - material: Vec, - ) -> Vec { - ephemeral_key - .to_bytes() - .iter() - .cloned() - .chain(material) - .collect() - } - - fn prepare_finalization_response() -> Vec { - vec![1] - } -} - -impl<'a> Future for GatewayHandshake<'a> { - type Output = Result; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - Pin::new(&mut self.handshake_future).poll(cx) + pub(crate) async fn perform_gateway_handshake( + mut self, + raw_init_message: Vec, + ) -> Result + where + S: Stream + Sink + Unpin, + { + let handshake_res = self.gateway_handshake_inner(raw_init_message).await; + self.check_for_handshake_processing_error(handshake_res) + .await?; + Ok(self.finalize_handshake()) } } diff --git a/common/gateway-requests/src/registration/handshake/messages.rs b/common/gateway-requests/src/registration/handshake/messages.rs new file mode 100644 index 0000000000..e2a823c5f9 --- /dev/null +++ b/common/gateway-requests/src/registration/handshake/messages.rs @@ -0,0 +1,213 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::registration::handshake::error::HandshakeError; +use nym_crypto::asymmetric::{ed25519, x25519}; +use nym_crypto::symmetric::aead::nonce_size; +use nym_sphinx::params::GatewayEncryptionAlgorithm; +use std::iter::once; + +// it is vital nobody changes the serialisation implementation unless you have an EXTREMELY good reason, +// as otherwise you have very high chance of breaking backwards compatibility +pub trait HandshakeMessage { + fn into_bytes(self) -> Vec; + + fn try_from_bytes(bytes: &[u8]) -> Result + where + Self: Sized; +} + +pub struct Initialisation { + pub identity: ed25519::PublicKey, + pub ephemeral_dh: x25519::PublicKey, + pub derive_aes256_gcm_siv_key: bool, +} + +pub struct MaterialExchange { + pub signature_ciphertext: [u8; ed25519::SIGNATURE_LENGTH], + pub nonce: Option>, +} + +impl MaterialExchange { + pub fn attach_ephemeral_dh(self, ephemeral_dh: x25519::PublicKey) -> GatewayMaterialExchange { + GatewayMaterialExchange { + ephemeral_dh, + materials: self, + } + } +} + +pub struct GatewayMaterialExchange { + pub ephemeral_dh: x25519::PublicKey, + pub materials: MaterialExchange, +} + +pub struct Finalization { + pub success: bool, +} + +impl Finalization { + pub fn ensure_success(&self) -> Result<(), HandshakeError> { + if !self.success { + return Err(HandshakeError::HandshakeFailure); + } + Ok(()) + } +} + +impl HandshakeMessage for Initialisation { + // LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_NON_LEGACY + // 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 + .to_bytes() + .into_iter() + .chain(self.ephemeral_dh.to_bytes()); + + if self.derive_aes256_gcm_siv_key { + bytes.chain(once(1)).collect() + } else { + bytes.collect() + } + } + + // this will need to be adjusted when REMOTE_ID_PUBKEY is removed + fn try_from_bytes(bytes: &[u8]) -> Result + where + Self: Sized, + { + let legacy_len = ed25519::PUBLIC_KEY_LENGTH + x25519::PUBLIC_KEY_SIZE; + let current_len = legacy_len + 1; + if bytes.len() != legacy_len && bytes.len() != current_len { + return Err(HandshakeError::MalformedRequest); + } + + let identity = ed25519::PublicKey::from_bytes(&bytes[..ed25519::PUBLIC_KEY_LENGTH]) + .map_err(|_| HandshakeError::MalformedRequest)?; + + // this can only fail if the provided bytes have len different from encryption::PUBLIC_KEY_SIZE + // which is impossible + let ephemeral_dh = + x25519::PublicKey::from_bytes(&bytes[ed25519::PUBLIC_KEY_LENGTH..legacy_len]).unwrap(); + + let derive_aes256_gcm_siv_key = if bytes.len() == legacy_len { + false + } else { + if bytes[legacy_len] != 1 { + return Err(HandshakeError::MalformedRequest); + } + true + }; + + Ok(Initialisation { + identity, + ephemeral_dh, + derive_aes256_gcm_siv_key, + }) + } +} + +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.iter().cloned().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 + AES256-GCM-SIV nonce (76 bytes) + let legacy_len = ed25519::SIGNATURE_LENGTH; + let current_len = legacy_len + nonce_size::(); + + if bytes.len() != legacy_len && bytes.len() != current_len { + return Err(HandshakeError::MalformedResponse); + } + + let mut signature_ciphertext = [0u8; ed25519::SIGNATURE_LENGTH]; + signature_ciphertext.copy_from_slice(&bytes[..legacy_len]); + + let nonce = if bytes.len() == current_len { + Some(bytes[legacy_len..].to_vec()) + } else { + None + }; + + Ok(MaterialExchange { + signature_ciphertext, + nonce, + }) + } +} + +impl HandshakeMessage for GatewayMaterialExchange { + // G^y || AES(k, SIG(PRIV_GATE, G^y || G^x)) + fn into_bytes(self) -> Vec { + self.ephemeral_dh + .to_bytes() + .into_iter() + .chain(self.materials.into_bytes().into_iter()) + .collect() + } + + fn try_from_bytes(bytes: &[u8]) -> Result + where + Self: Sized, + { + // we expect to receive either: + // LEGACY: x25519 pubkey + ed25519 signature ciphertext (96 bytes) + // CURRENT: x25519 pubkey + ed25519 signature ciphertext + AES256-GCM-SIV nonce (112 bytes) + let legacy_len = x25519::PUBLIC_KEY_SIZE + ed25519::SIGNATURE_LENGTH; + let current_len = legacy_len + nonce_size::(); + + if bytes.len() != legacy_len && bytes.len() != current_len { + return Err(HandshakeError::MalformedResponse); + } + + // this can only fail if the provided bytes have len different from PUBLIC_KEY_SIZE + // which is impossible + let ephemeral_dh = + x25519::PublicKey::from_bytes(&bytes[..x25519::PUBLIC_KEY_SIZE]).unwrap(); + let materials = MaterialExchange::try_from_bytes(&bytes[x25519::PUBLIC_KEY_SIZE..])?; + + Ok(GatewayMaterialExchange { + ephemeral_dh, + materials, + }) + } +} + +impl HandshakeMessage for Finalization { + fn into_bytes(self) -> Vec { + if self.success { + vec![1] + } else { + vec![0] + } + } + + fn try_from_bytes(bytes: &[u8]) -> Result + where + Self: Sized, + { + if bytes.len() != 1 { + return Err(HandshakeError::MalformedResponse); + } + + let success = if bytes[0] == 1 { true } else { false }; + Ok(Finalization { success }) + } +} diff --git a/common/gateway-requests/src/registration/handshake/mod.rs b/common/gateway-requests/src/registration/handshake/mod.rs index 56804f8f55..f0a4032a88 100644 --- a/common/gateway-requests/src/registration/handshake/mod.rs +++ b/common/gateway-requests/src/registration/handshake/mod.rs @@ -1,16 +1,17 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use self::client::ClientHandshake; use self::error::HandshakeError; +use crate::registration::handshake::state::State; +use futures::future::BoxFuture; use futures::{Sink, Stream}; use nym_crypto::asymmetric::identity; use rand::{CryptoRng, RngCore}; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; use tungstenite::{Error as WsError, Message as WsMessage}; -#[cfg(not(target_arch = "wasm32"))] -use self::gateway::GatewayHandshake; - #[cfg(not(target_arch = "wasm32"))] use nym_task::TaskClient; @@ -20,52 +21,73 @@ mod client; pub mod error; #[cfg(not(target_arch = "wasm32"))] mod gateway; +mod messages; mod shared_key; mod state; pub use self::shared_key::legacy::{LegacySharedKeySize, LegacySharedKeys}; -pub use self::shared_key::{SharedKeyConversionError, SharedSymmetricKey}; +pub use self::shared_key::{ + SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey, +}; // Note: the handshake is built on top of WebSocket, but in principle it shouldn't be too difficult // to remove that restriction, by just changing Sink and Stream into // AsyncWrite and AsyncRead and slightly adjusting the implementation. But right now // we do not need to worry about that. -pub async fn client_handshake<'a, S>( +pub struct GatewayHandshake<'a> { + handshake_future: BoxFuture<'a, Result>, +} + +impl<'a> Future for GatewayHandshake<'a> { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Pin::new(&mut self.handshake_future).poll(cx) + } +} + +pub fn client_handshake<'a, S>( rng: &mut (impl RngCore + CryptoRng), ws_stream: &'a mut S, identity: &'a identity::KeyPair, gateway_pubkey: identity::PublicKey, - expects_credential_usage: bool, + derive_aes256_gcm_siv_key: bool, #[cfg(not(target_arch = "wasm32"))] shutdown: TaskClient, -) -> Result +) -> GatewayHandshake<'a> where S: Stream + Sink + Unpin + Send + 'a, { - ClientHandshake::new( + let state = State::new( rng, ws_stream, identity, - gateway_pubkey, - expects_credential_usage, + Some(gateway_pubkey), #[cfg(not(target_arch = "wasm32"))] shutdown, ) - .await + .with_aes256_gcm_siv_key(derive_aes256_gcm_siv_key); + + GatewayHandshake { + handshake_future: Box::pin(state.perform_client_handshake()), + } } #[cfg(not(target_arch = "wasm32"))] -pub async fn gateway_handshake<'a, S>( +pub fn gateway_handshake<'a, S>( rng: &mut (impl RngCore + CryptoRng), ws_stream: &'a mut S, identity: &'a identity::KeyPair, received_init_payload: Vec, shutdown: TaskClient, -) -> Result +) -> GatewayHandshake<'a> where S: Stream + Sink + Unpin + Send + 'a, { - GatewayHandshake::new(rng, ws_stream, identity, received_init_payload, shutdown).await + let state = State::new(rng, ws_stream, identity, None, shutdown); + GatewayHandshake { + handshake_future: Box::pin(state.perform_gateway_handshake(received_init_payload)), + } } /* diff --git a/common/gateway-requests/src/registration/handshake/shared_key/legacy.rs b/common/gateway-requests/src/registration/handshake/shared_key/legacy.rs index 79c7382cff..9ba6581ddd 100644 --- a/common/gateway-requests/src/registration/handshake/shared_key/legacy.rs +++ b/common/gateway-requests/src/registration/handshake/shared_key/legacy.rs @@ -1,8 +1,8 @@ // Copyright 2020-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::registration::handshake::shared_key::SharedKeyConversionError; -use crate::{GatewayMacSize, GatewayRequestsError}; +use crate::registration::handshake::shared_key::{SharedKeyConversionError, SharedKeyUsageError}; +use crate::GatewayMacSize; use nym_crypto::generic_array::{ typenum::{Sum, Unsigned, U16}, GenericArray, @@ -84,10 +84,10 @@ impl LegacySharedKeys { &self, enc_data: &[u8], iv: Option<&IV>, - ) -> Result, GatewayRequestsError> { + ) -> Result, SharedKeyUsageError> { let mac_size = GatewayMacSize::to_usize(); if enc_data.len() < mac_size { - return Err(GatewayRequestsError::TooShortRequest); + return Err(SharedKeyUsageError::TooShortRequest); } let mac_tag = &enc_data[..mac_size]; @@ -98,7 +98,7 @@ impl LegacySharedKeys { message_bytes, mac_tag, ) { - return Err(GatewayRequestsError::InvalidMac); + return Err(SharedKeyUsageError::InvalidMac); } // couldn't have made the first borrow mutable as you can't have an immutable borrow diff --git a/common/gateway-requests/src/registration/handshake/shared_key/mod.rs b/common/gateway-requests/src/registration/handshake/shared_key/mod.rs index 0c7b13f18d..7c89546f35 100644 --- a/common/gateway-requests/src/registration/handshake/shared_key/mod.rs +++ b/common/gateway-requests/src/registration/handshake/shared_key/mod.rs @@ -1,17 +1,109 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::GatewayRequestsError; +use crate::registration::handshake::LegacySharedKeys; use nym_crypto::generic_array::{typenum::Unsigned, GenericArray}; -use nym_crypto::symmetric::aead::{self, AeadKey, KeySizeUser, Nonce}; +use nym_crypto::symmetric::aead::{self, nonce_size, AeadError, AeadKey, KeySizeUser, Nonce}; +use nym_crypto::symmetric::stream_cipher::{iv_size, IV}; use nym_pemstore::traits::PemStorableKey; -use nym_sphinx::params::GatewayEncryptionAlgorithm; +use nym_sphinx::params::{GatewayEncryptionAlgorithm, LegacyGatewayEncryptionAlgorithm}; use serde::{Deserialize, Serialize}; use thiserror::Error; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; pub mod legacy; +pub type SharedKeySize = ::KeySize; + +#[derive(Debug, PartialEq)] +pub enum SharedGatewayKey { + Current(SharedSymmetricKey), + Legacy(LegacySharedKeys), +} + +#[derive(Debug, Error)] +pub enum SharedKeyUsageError { + #[error("the request is too short")] + TooShortRequest, + + #[error("provided MAC is invalid")] + InvalidMac, + + #[error("the provided nonce (or legacy IV) did not have the expected length")] + MalformedNonce, + + #[error("did not provide a valid nonce for aead encryption")] + MissingAeadNonce, + + #[error("failed to either encrypt or decrypt provided message")] + AeadFailure(#[from] AeadError), +} + +impl SharedGatewayKey { + fn 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 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::aead_nonce(raw_nonce)?; + aes_gcm_siv.encrypt(plaintext, &nonce) + } + SharedGatewayKey::Legacy(aes_ctr) => { + let iv = Self::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::aead_nonce(raw_nonce)?; + aes_gcm_siv.decrypt(ciphertext, &nonce) + } + SharedGatewayKey::Legacy(aes_ctr) => { + let iv = Self::cipher_iv(raw_nonce)?; + aes_ctr.decrypt_tagged(ciphertext, iv) + } + } + } +} + #[derive(Debug, PartialEq, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)] pub struct SharedSymmetricKey(AeadKey); @@ -60,16 +152,16 @@ impl SharedSymmetricKey { &self, plaintext: &[u8], nonce: &Nonce, - ) -> Result, GatewayRequestsError> { - aead::encrypt::(&self.0, &nonce, plaintext).map_err(Into::into) + ) -> Result, SharedKeyUsageError> { + aead::encrypt::(&self.0, nonce, plaintext).map_err(Into::into) } pub fn decrypt( &self, ciphertext: &[u8], nonce: &Nonce, - ) -> Result, GatewayRequestsError> { - aead::decrypt::(&self.0, &nonce, ciphertext).map_err(Into::into) + ) -> Result, SharedKeyUsageError> { + aead::decrypt::(&self.0, nonce, ciphertext).map_err(Into::into) } } diff --git a/common/gateway-requests/src/registration/handshake/state.rs b/common/gateway-requests/src/registration/handshake/state.rs index 8a7351d22f..06ba92f9d3 100644 --- a/common/gateway-requests/src/registration/handshake/state.rs +++ b/common/gateway-requests/src/registration/handshake/state.rs @@ -2,26 +2,31 @@ // SPDX-License-Identifier: Apache-2.0 use crate::registration::handshake::error::HandshakeError; -use crate::registration::handshake::WsItem; -use crate::registration::handshake::{LegacySharedKeySize, LegacySharedKeys}; +use crate::registration::handshake::messages::{ + Finalization, HandshakeMessage, Initialisation, MaterialExchange, +}; +use crate::registration::handshake::shared_key::SharedKeySize; +use crate::registration::handshake::{LegacySharedKeySize, LegacySharedKeys, SharedSymmetricKey}; +use crate::registration::handshake::{SharedGatewayKey, WsItem}; use crate::types; use futures::{Sink, SinkExt, Stream, StreamExt}; +use nym_crypto::asymmetric::{ed25519, x25519}; +use nym_crypto::symmetric::aead::random_nonce; use nym_crypto::{ asymmetric::{encryption, identity}, generic_array::typenum::Unsigned, hkdf, - symmetric::stream_cipher, }; -use nym_sphinx::params::{GatewaySharedKeyHkdfAlgorithm, LegacyGatewayEncryptionAlgorithm}; -#[cfg(not(target_arch = "wasm32"))] -use nym_task::TaskClient; -use rand::{CryptoRng, RngCore}; -use tracing::log::*; - +use nym_sphinx::params::{GatewayEncryptionAlgorithm, GatewaySharedKeyHkdfAlgorithm}; +use rand::{thread_rng, CryptoRng, RngCore}; use std::str::FromStr; use std::time::Duration; +use tracing::log::*; use tungstenite::Message as WsMessage; +#[cfg(not(target_arch = "wasm32"))] +use nym_task::TaskClient; + #[cfg(not(target_arch = "wasm32"))] use tokio::time::timeout; @@ -35,21 +40,20 @@ pub(crate) struct State<'a, S> { /// Identity of the local "node" (client or gateway) which is used /// during the handshake. - identity: &'a identity::KeyPair, + identity: &'a ed25519::KeyPair, /// Local ephemeral Diffie-Hellman keypair generated as a part of the handshake. - ephemeral_keypair: encryption::KeyPair, + 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, + remote_pubkey: Option, - // this field is really out of place here, however, we need to propagate this information somehow - // in order to establish correct protocol for backwards compatibility reasons - expects_credential_usage: bool, + /// Specifies whether the end product should be an AES128Ctr + blake3 HMAC keys (legacy) or AES256-GCM-SIV (current) + derive_aes256_gcm_siv_key: bool, // channel to receive shutdown signal #[cfg(not(target_arch = "wasm32"))] @@ -62,7 +66,6 @@ impl<'a, S> State<'a, S> { ws_stream: &'a mut S, identity: &'a identity::KeyPair, remote_pubkey: Option, - expects_credential_usage: bool, #[cfg(not(target_arch = "wasm32"))] shutdown: TaskClient, ) -> Self { let ephemeral_keypair = encryption::KeyPair::new(rng); @@ -72,49 +75,40 @@ impl<'a, S> State<'a, S> { identity, remote_pubkey, derived_shared_keys: None, - expects_credential_usage, + // later on this should become the default + derive_aes256_gcm_siv_key: false, #[cfg(not(target_arch = "wasm32"))] shutdown, } } + pub(crate) fn with_aes256_gcm_siv_key(mut self, derive_aes256_gcm_siv_key: bool) -> Self { + self.derive_aes256_gcm_siv_key = derive_aes256_gcm_siv_key; + self + } + + pub(crate) fn set_aes256_gcm_siv_key_derivation(&mut self, derive_aes256_gcm_siv_key: bool) { + self.derive_aes256_gcm_siv_key = derive_aes256_gcm_siv_key; + } + #[cfg(not(target_arch = "wasm32"))] pub(crate) fn local_ephemeral_key(&self) -> &encryption::PublicKey { self.ephemeral_keypair.public_key() } - // LOCAL_ID_PUBKEY || EPHEMERAL_KEY + // LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_NON_LEGACY // Eventually the ID_PUBKEY prefix will get removed and recipient will know // initializer's identity from another source. - pub(crate) fn init_message(&self) -> Vec { - self.identity - .public_key() - .to_bytes() - .into_iter() - .chain(self.ephemeral_keypair.public_key().to_bytes()) - .collect() - } - - // this will need to be adjusted when REMOTE_ID_PUBKEY is removed - #[cfg(not(target_arch = "wasm32"))] - pub(crate) fn parse_init_message( - mut init_message: Vec, - ) -> Result<(identity::PublicKey, encryption::PublicKey), HandshakeError> { - if init_message.len() != identity::PUBLIC_KEY_LENGTH + encryption::PUBLIC_KEY_SIZE { - return Err(HandshakeError::MalformedRequest); + pub(crate) fn init_message(&self) -> Initialisation { + Initialisation { + identity: *self.identity.public_key(), + ephemeral_dh: *self.ephemeral_keypair.public_key(), + derive_aes256_gcm_siv_key: self.derive_aes256_gcm_siv_key, } + } - let remote_ephemeral_key_bytes = init_message.split_off(identity::PUBLIC_KEY_LENGTH); - // this can only fail if the provided bytes have len different from encryption::PUBLIC_KEY_SIZE - // which is impossible - let remote_ephemeral_key = - encryption::PublicKey::from_bytes(&remote_ephemeral_key_bytes).unwrap(); - - // this could actually fail if the curve point fails to get decompressed - let remote_identity = identity::PublicKey::from_bytes(&init_message) - .map_err(|_| HandshakeError::MalformedRequest)?; - - Ok((remote_identity, remote_ephemeral_key)) + pub(crate) fn finalization_message(&self) -> Finalization { + Finalization { success: true } } pub(crate) fn derive_shared_key(&mut self, remote_ephemeral_key: &encryption::PublicKey) { @@ -123,19 +117,28 @@ impl<'a, S> State<'a, S> { .private_key() .diffie_hellman(remote_ephemeral_key); + let key_size = if self.derive_aes256_gcm_siv_key { + SharedKeySize::to_usize() + } else { + LegacySharedKeySize::to_usize() + }; + // there is no reason for this to fail as our okm is expected to be only 16 bytes let okm = hkdf::extract_then_expand::( - None, - &dh_result, - None, - LegacySharedKeySize::to_usize(), + None, &dh_result, None, key_size, ) .expect("somehow too long okm was provided"); - let derived_shared_key = - LegacySharedKeys::try_from_bytes(&okm).expect("okm was expanded to incorrect length!"); - - self.derived_shared_keys = Some(derived_shared_key) + let shared_key = if self.derive_aes256_gcm_siv_key { + let current_key = SharedSymmetricKey::try_from_bytes(&okm) + .expect("okm was expanded to incorrect length!"); + SharedGatewayKey::Current(current_key) + } else { + let legacy_key = LegacySharedKeys::try_from_bytes(&okm) + .expect("okm was expanded to incorrect length!"); + SharedGatewayKey::Legacy(legacy_key) + }; + self.derived_shared_keys = Some(shared_key) } // produces AES(k, SIG(ID_PRIV, G^x || G^y), @@ -143,47 +146,59 @@ impl<'a, S> State<'a, S> { pub(crate) fn prepare_key_material_sig( &self, remote_ephemeral_key: &encryption::PublicKey, - ) -> Vec { - let message: Vec<_> = self + ) -> Result { + let plaintext: Vec<_> = self .ephemeral_keypair .public_key() .to_bytes() .into_iter() .chain(remote_ephemeral_key.to_bytes()) .collect(); + let signature = self.identity.private_key().sign(plaintext); - let signature = self.identity.private_key().sign(message); - let zero_iv = stream_cipher::zero_iv::(); - stream_cipher::encrypt::( - self.derived_shared_keys.as_ref().unwrap().encryption_key(), - &zero_iv, - &signature.to_bytes(), - ) + let nonce = if self.derive_aes256_gcm_siv_key { + let mut rng = thread_rng(); + Some(random_nonce::(&mut rng).to_vec()) + } else { + None + }; + + // SAFETY: this function is only called after the local key has already been derived + let ciphertext = self + .derived_shared_keys + .as_ref() + .expect("shared key was not derived!") + .encrypt(&signature.to_bytes(), nonce.as_deref())?; + let mut signature_ciphertext = [0u8; ed25519::SIGNATURE_LENGTH]; + signature_ciphertext.copy_from_slice(&ciphertext); + + Ok(MaterialExchange { + signature_ciphertext, + nonce, + }) } - // must be called after shared key was derived locally and remote's identity is known pub(crate) fn verify_remote_key_material( &self, - remote_material: &[u8], - remote_ephemeral_key: &encryption::PublicKey, + remote_response: &MaterialExchange, + remote_ephemeral_key: &x25519::PublicKey, ) -> Result<(), HandshakeError> { - if remote_material.len() != identity::SIGNATURE_LENGTH { - return Err(HandshakeError::KeyMaterialOfInvalidSize( - remote_material.len(), - )); - } + // SAFETY: this function is only called after the local key has already been derived let derived_shared_key = self .derived_shared_keys .as_ref() .expect("shared key was not derived!"); + // if the [client] init message contained non-legacy flag, the associated nonce MUST be present + if self.derive_aes256_gcm_siv_key && remote_response.nonce.is_none() { + return Err(HandshakeError::MissingNonceForCurrentKey); + } + // first decrypt received data - let zero_iv = stream_cipher::zero_iv::(); - let decrypted_signature = stream_cipher::decrypt::( - derived_shared_key.encryption_key(), - &zero_iv, - remote_material, - ); + let decrypted_signature = derived_shared_key.decrypt( + &remote_response.signature_ciphertext, + remote_response.nonce.as_deref(), + )?; // now verify signature itself let signature = identity::Signature::from_bytes(&decrypted_signature) @@ -246,7 +261,7 @@ impl<'a, S> State<'a, S> { } #[cfg(not(target_arch = "wasm32"))] - async fn _receive_handshake_message(&mut self) -> Result, HandshakeError> + async fn _receive_handshake_message_bytes(&mut self) -> Result, HandshakeError> where S: Stream + Unpin, { @@ -278,14 +293,20 @@ impl<'a, S> State<'a, S> { } } - pub(crate) async fn receive_handshake_message(&mut self) -> Result, HandshakeError> + pub(crate) async fn receive_handshake_message(&mut self) -> Result where S: Stream + Unpin, + M: HandshakeMessage, { // TODO: make timeout duration configurable - timeout(Duration::from_secs(5), self._receive_handshake_message()) - .await - .map_err(|_| HandshakeError::Timeout)? + let bytes = timeout( + Duration::from_secs(5), + self._receive_handshake_message_bytes(), + ) + .await + .map_err(|_| HandshakeError::Timeout)??; + + M::try_from_bytes(&bytes) } // upon receiving this, the receiver should terminate the handshake @@ -305,13 +326,13 @@ impl<'a, S> State<'a, S> { pub(crate) async fn send_handshake_data( &mut self, - payload: Vec, + inner_message: impl HandshakeMessage, ) -> Result<(), HandshakeError> where S: Sink + Unpin, { let handshake_message = - types::RegistrationHandshake::new_payload(payload, self.expects_credential_usage); + types::RegistrationHandshake::new_payload(inner_message.into_bytes()); self.ws_stream .send(WsMessage::Text(handshake_message.try_into().unwrap())) .await @@ -320,7 +341,26 @@ impl<'a, S> State<'a, S> { /// Finish the handshake, yielding the derived shared key and implicitly dropping all borrowed /// values. - pub(crate) fn finalize_handshake(self) -> LegacySharedKeys { + pub(crate) fn finalize_handshake(self) -> SharedGatewayKey { self.derived_shared_keys.unwrap() } + + // If any step along the way failed (that are non-network related), + // try to send 'error' message to the remote + // party to indicate handshake should be terminated + pub(crate) async fn check_for_handshake_processing_error( + &mut self, + result: Result, + ) -> Result + where + S: Sink + Unpin, + { + match result { + Ok(ok) => Ok(ok), + Err(err) => { + self.send_handshake_error(err.to_string()).await?; + Err(err) + } + } + } } diff --git a/common/gateway-requests/src/types.rs b/common/gateway-requests/src/types.rs index d55ca70b72..ec5e7f5056 100644 --- a/common/gateway-requests/src/types.rs +++ b/common/gateway-requests/src/types.rs @@ -4,7 +4,7 @@ use crate::authentication::encrypted_address::EncryptedAddressBytes; use crate::iv::{IVConversionError, IV}; use crate::models::CredentialSpendingRequest; -use crate::registration::handshake::LegacySharedKeys; +use crate::registration::handshake::{LegacySharedKeys, SharedKeyUsageError}; use crate::{GatewayMacSize, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION}; use nym_credentials::ecash::bandwidth::CredentialSpendingData; use nym_credentials_interface::CompactEcashError; @@ -17,12 +17,10 @@ use nym_sphinx::params::packet_sizes::PacketSize; use nym_sphinx::params::{GatewayIntegrityHmacAlgorithm, LegacyGatewayEncryptionAlgorithm}; use nym_sphinx::DestinationAddressBytes; use serde::{Deserialize, Serialize}; -use tracing::log::error; - -use nym_crypto::symmetric::aead::AeadError; use std::str::FromStr; use std::string::FromUtf8Error; use thiserror::Error; +use tracing::log::error; use tungstenite::protocol::Message; #[derive(Serialize, Deserialize, Debug)] @@ -39,17 +37,9 @@ pub enum RegistrationHandshake { } impl RegistrationHandshake { - pub fn new_payload(data: Vec, will_use_credentials: bool) -> Self { - // if we're not going to be using credentials, advertise lower protocol version to allow connection - // to wider range of gateways - let protocol_version = if will_use_credentials { - Some(CURRENT_PROTOCOL_VERSION) - } else { - Some(INITIAL_PROTOCOL_VERSION) - }; - + pub fn new_payload(data: Vec) -> Self { RegistrationHandshake::HandshakePayload { - protocol_version, + protocol_version: Some(CURRENT_PROTOCOL_VERSION), data, } } @@ -104,6 +94,9 @@ impl SimpleGatewayRequestsError { #[derive(Debug, Error)] pub enum GatewayRequestsError { + #[error(transparent)] + KeyUsageFailure(#[from] SharedKeyUsageError), + #[error("the request is too short")] TooShortRequest, @@ -155,9 +148,6 @@ pub enum GatewayRequestsError { #[error("the provided [v1] credential has invalid number of parameters - {0}")] InvalidNumberOfEmbededParameters(u32), - #[error("failed to either encrypt or decrypt provided message")] - AeadFailure(#[from] AeadError), - // variant to catch legacy errors #[error("{0}")] Other(String), diff --git a/common/nymsphinx/params/Cargo.toml b/common/nymsphinx/params/Cargo.toml index bf477580f4..9c360942b0 100644 --- a/common/nymsphinx/params/Cargo.toml +++ b/common/nymsphinx/params/Cargo.toml @@ -11,7 +11,7 @@ repository = { workspace = true } thiserror = { workspace = true } serde = { workspace = true, features = ["derive"] } -nym-crypto = { path = "../../crypto", features = ["hashing", "stream_cipher", "aes-gcm-siv"] } +nym-crypto = { path = "../../crypto", features = ["hashing", "stream_cipher", "aead"] } nym-sphinx-types = { path = "../types" } [features] 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 a2337c8955..4b445231b3 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -21,6 +21,7 @@ use nym_crypto::asymmetric::identity; use nym_gateway_requests::authentication::encrypted_address::{ EncryptedAddressBytes, EncryptedAddressConversionError, }; +use nym_gateway_requests::registration::handshake::SharedGatewayKey; use nym_gateway_requests::{ iv::{IVConversionError, IV}, registration::handshake::{ @@ -178,7 +179,7 @@ where async fn perform_registration_handshake( &mut self, init_msg: Vec, - ) -> Result + ) -> Result where S: AsyncRead + AsyncWrite + Unpin + Send, { From 94113206b21e69bb79a4b67552df723a3cd7843a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 16 Sep 2024 10:13:17 +0100 Subject: [PATCH 03/17] completing handshake using legacy keys --- .../gateway-client/src/client/mod.rs | 20 +++++--- common/crypto/src/symmetric/aead.rs | 7 +++ .../src/registration/handshake/client.rs | 1 + .../src/registration/handshake/messages.rs | 34 ++++++++----- .../src/registration/handshake/mod.rs | 2 + .../handshake/shared_key/legacy.rs | 39 +++++++++++--- .../registration/handshake/shared_key/mod.rs | 38 ++++++++++++++ .../src/registration/handshake/state.rs | 51 +++++++++++++++---- common/gateway-requests/src/types.rs | 4 +- .../websocket/connection_handler/fresh.rs | 5 ++ 10 files changed, 161 insertions(+), 40 deletions(-) diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index 40dd11d670..1a29bba376 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -19,9 +19,7 @@ use nym_credentials::CredentialSpendingData; use nym_crypto::asymmetric::identity; use nym_gateway_requests::authentication::encrypted_address::EncryptedAddressBytes; use nym_gateway_requests::iv::IV; -use nym_gateway_requests::registration::handshake::{ - client_handshake, LegacySharedKeys, SharedGatewayKey, -}; +use nym_gateway_requests::registration::handshake::{client_handshake, LegacySharedKeys}; use nym_gateway_requests::{ BinaryRequest, ClientControlRequest, ServerResponse, AES_GCM_SIV_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION, @@ -31,6 +29,7 @@ use nym_task::TaskClient; use nym_validator_client::nyxd::contract_traits::DkgQueryClient; use rand::rngs::OsRng; use std::sync::Arc; +use tracing::instrument; use tracing::*; use tungstenite::protocol::Message; use url::Url; @@ -44,8 +43,6 @@ use tokio_tungstenite::connect_async; #[cfg(not(unix))] use std::os::raw::c_int as RawFd; -use std::time::Duration; -use tracing::instrument; #[cfg(target_arch = "wasm32")] use wasm_utils::websocket::JSWebsocket; #[cfg(target_arch = "wasm32")] @@ -403,13 +400,19 @@ impl GatewayClient { } } - async fn register(&mut self, legacy: bool) -> Result<(), GatewayClientError> { + async fn register( + &mut self, + derive_aes256_gcm_siv_key: bool, + ) -> Result<(), GatewayClientError> { if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } debug_assert!(self.connection.is_available()); - log::debug!("registering with gateway. using legacy key derivation: {legacy}"); + log::debug!( + "registering with gateway. using legacy key derivation: {}", + !derive_aes256_gcm_siv_key + ); // 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 @@ -422,6 +425,7 @@ impl GatewayClient { self.local_identity.as_ref(), self.gateway_identity, self.cfg.bandwidth.require_tickets, + derive_aes256_gcm_siv_key, #[cfg(not(target_arch = "wasm32"))] self.task_client.clone(), ) @@ -559,7 +563,7 @@ impl GatewayClient { if self.shared_key.is_some() { self.authenticate(None, supports_aes_gcm_siv).await?; } else { - self.register(!supports_aes_gcm_siv).await?; + self.register(supports_aes_gcm_siv).await?; } if self.authenticated { // if we are authenticated it means we MUST have an associated shared_key diff --git a/common/crypto/src/symmetric/aead.rs b/common/crypto/src/symmetric/aead.rs index 2ac4716815..1a429ee208 100644 --- a/common/crypto/src/symmetric/aead.rs +++ b/common/crypto/src/symmetric/aead.rs @@ -36,6 +36,13 @@ where <::NonceSize>::to_usize() } +pub fn tag_size() -> usize +where + A: AeadCore, +{ + <::TagSize>::to_usize() +} + #[inline] pub fn encrypt<'msg, 'aad, A>( key: &AeadKey, diff --git a/common/gateway-requests/src/registration/handshake/client.rs b/common/gateway-requests/src/registration/handshake/client.rs index aebda07551..0acb6bff43 100644 --- a/common/gateway-requests/src/registration/handshake/client.rs +++ b/common/gateway-requests/src/registration/handshake/client.rs @@ -6,6 +6,7 @@ use crate::registration::handshake::state::State; use crate::registration::handshake::SharedGatewayKey; use crate::registration::handshake::{error::HandshakeError, WsItem}; use futures::{Sink, Stream}; +use tracing::info; use tungstenite::Message as WsMessage; impl<'a, S> State<'a, S> { diff --git a/common/gateway-requests/src/registration/handshake/messages.rs b/common/gateway-requests/src/registration/handshake/messages.rs index e2a823c5f9..0d3195aad5 100644 --- a/common/gateway-requests/src/registration/handshake/messages.rs +++ b/common/gateway-requests/src/registration/handshake/messages.rs @@ -3,7 +3,7 @@ use crate::registration::handshake::error::HandshakeError; use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_crypto::symmetric::aead::nonce_size; +use nym_crypto::symmetric::aead::{nonce_size, tag_size}; use nym_sphinx::params::GatewayEncryptionAlgorithm; use std::iter::once; @@ -17,14 +17,16 @@ pub trait HandshakeMessage { Self: Sized; } +#[derive(Debug)] pub struct Initialisation { pub identity: ed25519::PublicKey, pub ephemeral_dh: x25519::PublicKey, pub derive_aes256_gcm_siv_key: bool, } +#[derive(Debug)] pub struct MaterialExchange { - pub signature_ciphertext: [u8; ed25519::SIGNATURE_LENGTH], + pub signature_ciphertext: Vec, pub nonce: Option>, } @@ -37,11 +39,13 @@ impl MaterialExchange { } } +#[derive(Debug)] pub struct GatewayMaterialExchange { pub ephemeral_dh: x25519::PublicKey, pub materials: MaterialExchange, } +#[derive(Debug)] pub struct Finalization { pub success: bool, } @@ -119,7 +123,7 @@ impl HandshakeMessage for MaterialExchange { .chain(nonce) .collect() } else { - self.signature_ciphertext.iter().cloned().collect() + self.signature_ciphertext.to_vec() } } @@ -129,21 +133,25 @@ impl HandshakeMessage for MaterialExchange { { // we expect to receive either: // LEGACY: ed25519 signature ciphertext (64 bytes) - // CURRENT: ed25519 signature ciphertext + AES256-GCM-SIV nonce (76 bytes) + // CURRENT: ed25519 signature ciphertext (+ tag) + AES256-GCM-SIV nonce (76 bytes) let legacy_len = ed25519::SIGNATURE_LENGTH; - let current_len = legacy_len + nonce_size::(); + let current_len = legacy_len + + tag_size::() + + nonce_size::(); if bytes.len() != legacy_len && bytes.len() != current_len { return Err(HandshakeError::MalformedResponse); } - let mut signature_ciphertext = [0u8; ed25519::SIGNATURE_LENGTH]; - signature_ciphertext.copy_from_slice(&bytes[..legacy_len]); - - let nonce = if bytes.len() == current_len { - Some(bytes[legacy_len..].to_vec()) + 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 { - None + (bytes.to_vec(), None) }; Ok(MaterialExchange { @@ -159,7 +167,7 @@ impl HandshakeMessage for GatewayMaterialExchange { self.ephemeral_dh .to_bytes() .into_iter() - .chain(self.materials.into_bytes().into_iter()) + .chain(self.materials.into_bytes()) .collect() } @@ -207,7 +215,7 @@ impl HandshakeMessage for Finalization { return Err(HandshakeError::MalformedResponse); } - let success = if bytes[0] == 1 { true } else { false }; + let success = bytes[0] == 1; Ok(Finalization { success }) } } diff --git a/common/gateway-requests/src/registration/handshake/mod.rs b/common/gateway-requests/src/registration/handshake/mod.rs index f0a4032a88..1aae69f9fc 100644 --- a/common/gateway-requests/src/registration/handshake/mod.rs +++ b/common/gateway-requests/src/registration/handshake/mod.rs @@ -52,6 +52,7 @@ pub fn client_handshake<'a, S>( ws_stream: &'a mut S, identity: &'a identity::KeyPair, gateway_pubkey: identity::PublicKey, + expects_credential_usage: bool, derive_aes256_gcm_siv_key: bool, #[cfg(not(target_arch = "wasm32"))] shutdown: TaskClient, ) -> GatewayHandshake<'a> @@ -66,6 +67,7 @@ where #[cfg(not(target_arch = "wasm32"))] shutdown, ) + .with_credential_usage(expects_credential_usage) .with_aes256_gcm_siv_key(derive_aes256_gcm_siv_key); GatewayHandshake { diff --git a/common/gateway-requests/src/registration/handshake/shared_key/legacy.rs b/common/gateway-requests/src/registration/handshake/shared_key/legacy.rs index 9ba6581ddd..7b60a737b1 100644 --- a/common/gateway-requests/src/registration/handshake/shared_key/legacy.rs +++ b/common/gateway-requests/src/registration/handshake/shared_key/legacy.rs @@ -50,14 +50,14 @@ impl LegacySharedKeys { } /// 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( + /// 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 { - let encrypted_data = match iv { + match iv { Some(iv) => stream_cipher::encrypt::( self.encryption_key(), iv, @@ -71,13 +71,38 @@ impl LegacySharedKeys { 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(), - &encrypted_data, + &ciphertext, ); - mac.into_bytes().into_iter().chain(encrypted_data).collect() + 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( diff --git a/common/gateway-requests/src/registration/handshake/shared_key/mod.rs b/common/gateway-requests/src/registration/handshake/shared_key/mod.rs index 7c89546f35..b199e09752 100644 --- a/common/gateway-requests/src/registration/handshake/shared_key/mod.rs +++ b/common/gateway-requests/src/registration/handshake/shared_key/mod.rs @@ -102,6 +102,44 @@ impl SharedGatewayKey { } } } + + // 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::aead_nonce(raw_nonce)?; + aes_gcm_siv.encrypt(plaintext, &nonce) + } + SharedGatewayKey::Legacy(aes_ctr) => { + let iv = Self::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::aead_nonce(raw_nonce)?; + aes_gcm_siv.decrypt(ciphertext, &nonce) + } + SharedGatewayKey::Legacy(aes_ctr) => { + let iv = Self::cipher_iv(raw_nonce)?; + aes_ctr.decrypt_without_tag(ciphertext, iv) + } + } + } } #[derive(Debug, PartialEq, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)] diff --git a/common/gateway-requests/src/registration/handshake/state.rs b/common/gateway-requests/src/registration/handshake/state.rs index 06ba92f9d3..0522ae6631 100644 --- a/common/gateway-requests/src/registration/handshake/state.rs +++ b/common/gateway-requests/src/registration/handshake/state.rs @@ -8,7 +8,10 @@ use crate::registration::handshake::messages::{ use crate::registration::handshake::shared_key::SharedKeySize; use crate::registration::handshake::{LegacySharedKeySize, LegacySharedKeys, SharedSymmetricKey}; use crate::registration::handshake::{SharedGatewayKey, WsItem}; -use crate::types; +use crate::{ + types, AES_GCM_SIV_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, + INITIAL_PROTOCOL_VERSION, +}; use futures::{Sink, SinkExt, Stream, StreamExt}; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_crypto::symmetric::aead::random_nonce; @@ -19,6 +22,7 @@ use nym_crypto::{ }; use nym_sphinx::params::{GatewayEncryptionAlgorithm, GatewaySharedKeyHkdfAlgorithm}; use rand::{thread_rng, CryptoRng, RngCore}; +use std::any::{type_name, Any}; use std::str::FromStr; use std::time::Duration; use tracing::log::*; @@ -52,6 +56,10 @@ pub(crate) struct State<'a, S> { /// Ideally it would always be known before the handshake was initiated. remote_pubkey: Option, + // this field is really out of place here, however, we need to propagate this information somehow + // in order to establish correct protocol for backwards compatibility reasons + expects_credential_usage: bool, + /// Specifies whether the end product should be an AES128Ctr + blake3 HMAC keys (legacy) or AES256-GCM-SIV (current) derive_aes256_gcm_siv_key: bool, @@ -76,12 +84,18 @@ impl<'a, S> State<'a, S> { remote_pubkey, derived_shared_keys: None, // later on this should become the default + expects_credential_usage: false, derive_aes256_gcm_siv_key: false, #[cfg(not(target_arch = "wasm32"))] shutdown, } } + pub(crate) fn with_credential_usage(mut self, expects_credential_usage: bool) -> Self { + self.expects_credential_usage = expects_credential_usage; + self + } + pub(crate) fn with_aes256_gcm_siv_key(mut self, derive_aes256_gcm_siv_key: bool) -> Self { self.derive_aes256_gcm_siv_key = derive_aes256_gcm_siv_key; self @@ -156,6 +170,8 @@ impl<'a, S> State<'a, S> { .collect(); let signature = self.identity.private_key().sign(plaintext); + println!("signature len: {}", signature.to_bytes().len()); + let nonce = if self.derive_aes256_gcm_siv_key { let mut rng = thread_rng(); Some(random_nonce::(&mut rng).to_vec()) @@ -163,14 +179,14 @@ impl<'a, S> State<'a, S> { None }; + println!("key types: {:?}", self.derived_shared_keys); + // SAFETY: this function is only called after the local key has already been derived - let ciphertext = self + let signature_ciphertext = self .derived_shared_keys .as_ref() .expect("shared key was not derived!") - .encrypt(&signature.to_bytes(), nonce.as_deref())?; - let mut signature_ciphertext = [0u8; ed25519::SIGNATURE_LENGTH]; - signature_ciphertext.copy_from_slice(&ciphertext); + .encrypt_naive(&signature.to_bytes(), nonce.as_deref())?; Ok(MaterialExchange { signature_ciphertext, @@ -195,7 +211,7 @@ impl<'a, S> State<'a, S> { } // first decrypt received data - let decrypted_signature = derived_shared_key.decrypt( + let decrypted_signature = derived_shared_key.decrypt_naive( &remote_response.signature_ciphertext, remote_response.nonce.as_deref(), )?; @@ -324,15 +340,30 @@ impl<'a, S> State<'a, S> { .map_err(|_| HandshakeError::ClosedStream) } - pub(crate) async fn send_handshake_data( + fn request_protocol_version(&self) -> u8 { + if self.derive_aes256_gcm_siv_key { + AES_GCM_SIV_PROTOCOL_VERSION + } else if self.expects_credential_usage { + CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION + } else { + INITIAL_PROTOCOL_VERSION + } + } + + pub(crate) async fn send_handshake_data( &mut self, - inner_message: impl HandshakeMessage, + inner_message: M, ) -> Result<(), HandshakeError> where S: Sink + Unpin, + M: HandshakeMessage + Any, { - let handshake_message = - types::RegistrationHandshake::new_payload(inner_message.into_bytes()); + trace!("sending handshake message: {}", type_name::()); + + let handshake_message = types::RegistrationHandshake::new_payload( + inner_message.into_bytes(), + self.request_protocol_version(), + ); self.ws_stream .send(WsMessage::Text(handshake_message.try_into().unwrap())) .await diff --git a/common/gateway-requests/src/types.rs b/common/gateway-requests/src/types.rs index ec5e7f5056..82dab71853 100644 --- a/common/gateway-requests/src/types.rs +++ b/common/gateway-requests/src/types.rs @@ -37,9 +37,9 @@ pub enum RegistrationHandshake { } impl RegistrationHandshake { - pub fn new_payload(data: Vec) -> Self { + pub fn new_payload(data: Vec, protocol_version: u8) -> Self { RegistrationHandshake::HandshakePayload { - protocol_version: Some(CURRENT_PROTOCOL_VERSION), + protocol_version: Some(protocol_version), data, } } 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 4b445231b3..7d2d41fa7d 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -422,6 +422,11 @@ where return Ok(1); } + // a v3 gateway will understand v2 requests (legacy keys) + if client_protocol_version == 2 { + return Ok(2); + } + // 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 <= CURRENT_PROTOCOL_VERSION { From 21e9df488f402be576ce3e726a2ac51d998449df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 16 Sep 2024 17:20:39 +0100 Subject: [PATCH 04/17] compatibility with legacy clients --- Cargo.lock | 1 + .../20240916120000_add_aes256_gcm_siv_key.sql | 13 + .../client-core/gateways-storage/src/error.rs | 3 + .../client-core/gateways-storage/src/types.rs | 74 +++- .../client-core/src/client/base_client/mod.rs | 2 +- .../base_client/storage/migration_helpers.rs | 2 +- .../src/client/topology_control/mod.rs | 1 + common/client-core/src/init/types.rs | 4 +- .../gateway-client/src/client/mod.rs | 113 +++--- .../client-libs/gateway-client/src/error.rs | 5 +- common/client-libs/gateway-client/src/lib.rs | 5 +- .../gateway-client/src/socket_state.rs | 8 +- common/gateway-requests/Cargo.toml | 1 + .../src/authentication/encrypted_address.rs | 60 +-- common/gateway-requests/src/iv.rs | 76 ---- common/gateway-requests/src/lib.rs | 7 +- .../src/registration/handshake/client.rs | 1 - .../handshake/shared_key/legacy.rs | 10 +- .../registration/handshake/shared_key/mod.rs | 74 +++- .../src/registration/handshake/state.rs | 4 - common/gateway-requests/src/types.rs | 371 +++++++++++++----- common/gateway-storage/Cargo.toml | 1 + .../20240916120000_add_aes256_gcm_siv_key.sql | 13 + common/gateway-storage/src/error.rs | 3 + common/gateway-storage/src/lib.rs | 13 +- common/gateway-storage/src/models.rs | 30 +- common/gateway-storage/src/shared_keys.rs | 14 +- .../connection_handler/authenticated.rs | 7 +- .../websocket/connection_handler/fresh.rs | 86 ++-- .../websocket/connection_handler/mod.rs | 6 +- 30 files changed, 615 insertions(+), 393 deletions(-) create mode 100644 common/client-core/gateways-storage/fs_gateways_migrations/20240916120000_add_aes256_gcm_siv_key.sql delete mode 100644 common/gateway-requests/src/iv.rs create mode 100644 common/gateway-storage/migrations/20240916120000_add_aes256_gcm_siv_key.sql diff --git a/Cargo.lock b/Cargo.lock index c519976376..301e93c57e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5016,6 +5016,7 @@ dependencies = [ "rand", "serde", "serde_json", + "strum 0.26.3", "thiserror", "tokio", "tracing", diff --git a/common/client-core/gateways-storage/fs_gateways_migrations/20240916120000_add_aes256_gcm_siv_key.sql b/common/client-core/gateways-storage/fs_gateways_migrations/20240916120000_add_aes256_gcm_siv_key.sql new file mode 100644 index 0000000000..66a8632bb4 --- /dev/null +++ b/common/client-core/gateways-storage/fs_gateways_migrations/20240916120000_add_aes256_gcm_siv_key.sql @@ -0,0 +1,13 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + +-- make aes128 key column nullable and add aes256 column +ALTER TABLE remote_gateway_details RENAME COLUMN derived_aes128_ctr_blake3_hmac_keys_bs58 TO derived_aes128_ctr_blake3_hmac_keys_bs58_old; +ALTER TABLE remote_gateway_details ADD COLUMN derived_aes128_ctr_blake3_hmac_keys_bs58 TEXT; +ALTER TABLE remote_gateway_details ADD COLUMN derived_aes256_gcm_siv_key BLOB; + +UPDATE remote_gateway_details SET derived_aes128_ctr_blake3_hmac_keys_bs58 = derived_aes128_ctr_blake3_hmac_keys_bs58_old; + +ALTER TABLE remote_gateway_details DROP COLUMN derived_aes128_ctr_blake3_hmac_keys_bs58_old; \ No newline at end of file diff --git a/common/client-core/gateways-storage/src/error.rs b/common/client-core/gateways-storage/src/error.rs index c867557108..effac347fb 100644 --- a/common/client-core/gateways-storage/src/error.rs +++ b/common/client-core/gateways-storage/src/error.rs @@ -36,6 +36,9 @@ pub enum BadGateway { source: SharedKeyConversionError, }, + #[error("could not find any valid shared keys for gateway {gateway_id}")] + MissingSharedKey { gateway_id: String }, + #[error( "the listening address of gateway {gateway_id} ({raw_listener}) is malformed: {source}" )] diff --git a/common/client-core/gateways-storage/src/types.rs b/common/client-core/gateways-storage/src/types.rs index 94888faf9c..1b6cb35265 100644 --- a/common/client-core/gateways-storage/src/types.rs +++ b/common/client-core/gateways-storage/src/types.rs @@ -4,9 +4,12 @@ use crate::BadGateway; use cosmrs::AccountId; use nym_crypto::asymmetric::identity; -use nym_gateway_requests::registration::handshake::LegacySharedKeys; +use nym_gateway_requests::registration::handshake::{ + LegacySharedKeys, SharedGatewayKey, 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; @@ -64,13 +67,13 @@ impl From for GatewayRegistration { impl GatewayDetails { pub fn new_remote( gateway_id: identity::PublicKey, - derived_aes128_ctr_blake3_hmac_keys: Arc, + shared_key: Arc, gateway_owner_address: Option, gateway_listener: Url, ) -> Self { GatewayDetails::Remote(RemoteGatewayDetails { gateway_id, - derived_aes128_ctr_blake3_hmac_keys, + shared_key, gateway_owner_address, gateway_listener, }) @@ -87,9 +90,9 @@ impl GatewayDetails { } } - pub fn shared_key(&self) -> Option<&LegacySharedKeys> { + pub fn shared_key(&self) -> Option<&SharedGatewayKey> { match self { - GatewayDetails::Remote(details) => Some(&details.derived_aes128_ctr_blake3_hmac_keys), + GatewayDetails::Remote(details) => Some(&details.shared_key), GatewayDetails::Custom(_) => None, } } @@ -167,7 +170,8 @@ 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: String, + pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option, + pub derived_aes256_gcm_siv_key: Option>, pub gateway_owner_address: Option, pub gateway_listener: String, } @@ -184,15 +188,35 @@ impl TryFrom for RemoteGatewayDetails { } })?; - let derived_aes128_ctr_blake3_hmac_keys = Arc::new( - LegacySharedKeys::try_from_base58_string( + let shared_key = + match ( + &value.derived_aes256_gcm_siv_key, &value.derived_aes128_ctr_blake3_hmac_keys_bs58, - ) - .map_err(|source| BadGateway::MalformedSharedKeys { - gateway_id: value.gateway_id_bs58.clone(), - source, - })?, - ); + ) { + (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 @@ -218,7 +242,7 @@ impl TryFrom for RemoteGatewayDetails { Ok(RemoteGatewayDetails { gateway_id, - derived_aes128_ctr_blake3_hmac_keys, + shared_key: Arc::new(shared_key), gateway_owner_address, gateway_listener, }) @@ -227,11 +251,21 @@ impl TryFrom for RemoteGatewayDetails { impl<'a> From<&'a RemoteGatewayDetails> for RawRemoteGatewayDetails { fn from(value: &'a RemoteGatewayDetails) -> Self { + /* + pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option, + pub derived_aes256_gcm_siv_key: Option>, + */ + + 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: value - .derived_aes128_ctr_blake3_hmac_keys - .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(), } @@ -242,9 +276,7 @@ impl<'a> From<&'a RemoteGatewayDetails> for RawRemoteGatewayDetails { pub struct RemoteGatewayDetails { pub gateway_id: identity::PublicKey, - // note: `SharedKeys` implement ZeroizeOnDrop, meaning when `RemoteGatewayDetails` is dropped, - // the keys will be zeroized - pub derived_aes128_ctr_blake3_hmac_keys: Arc, + pub shared_key: Arc, pub gateway_owner_address: Option, diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index 0579cc12a8..ba2b20c8b4 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -387,7 +387,7 @@ where ), cfg, managed_keys.identity_keypair(), - Some(details.derived_aes128_ctr_blake3_hmac_keys), + Some(details.shared_key), packet_router, bandwidth_controller, shutdown, 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 91d7a0bf93..61c5f59f1d 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 @@ -91,7 +91,7 @@ pub mod v1_1_33 { .map_err(|err| ClientCoreError::UpgradeFailure { message: format!("the stored gateway id was malformed: {err}"), })?, - derived_aes128_ctr_blake3_hmac_keys: Arc::new(gateway_shared_key), + 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}"), diff --git a/common/client-core/src/client/topology_control/mod.rs b/common/client-core/src/client/topology_control/mod.rs index ae50128942..bbf32f377c 100644 --- a/common/client-core/src/client/topology_control/mod.rs +++ b/common/client-core/src/client/topology_control/mod.rs @@ -102,6 +102,7 @@ impl TopologyRefresher { .current_topology() .await .ok_or(NymTopologyError::EmptyNetworkTopology)?; + if !topology.gateway_exists(gateway) { return Err(NymTopologyError::NonExistentGatewayError { identity_key: gateway.to_base58_string(), diff --git a/common/client-core/src/init/types.rs b/common/client-core/src/init/types.rs index d0744abf62..2acf91311f 100644 --- a/common/client-core/src/init/types.rs +++ b/common/client-core/src/init/types.rs @@ -11,7 +11,7 @@ use nym_client_core_gateways_storage::{ }; use nym_crypto::asymmetric::identity; use nym_gateway_client::client::InitGatewayClient; -use nym_gateway_requests::registration::handshake::LegacySharedKeys; +use nym_gateway_requests::registration::handshake::SharedGatewayKey; use nym_sphinx::addressing::clients::Recipient; use nym_topology::gateway; use nym_validator_client::client::IdentityKey; @@ -104,7 +104,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, } diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index 1a29bba376..6ed2b98ec9 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -17,9 +17,7 @@ use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCred use nym_credential_storage::storage::Storage as CredentialStorage; use nym_credentials::CredentialSpendingData; use nym_crypto::asymmetric::identity; -use nym_gateway_requests::authentication::encrypted_address::EncryptedAddressBytes; -use nym_gateway_requests::iv::IV; -use nym_gateway_requests::registration::handshake::{client_handshake, LegacySharedKeys}; +use nym_gateway_requests::registration::handshake::{client_handshake, SharedGatewayKey}; use nym_gateway_requests::{ BinaryRequest, ClientControlRequest, ServerResponse, AES_GCM_SIV_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION, @@ -82,7 +80,7 @@ pub struct GatewayClient { gateway_address: String, gateway_identity: identity::PublicKey, local_identity: Arc, - shared_key: Option>, + shared_key: Option>, connection: SocketState, packet_router: PacketRouter, bandwidth_controller: Option>, @@ -100,7 +98,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>, task_client: TaskClient, @@ -432,9 +430,7 @@ impl GatewayClient { .await .map_err(GatewayClientError::RegistrationFailure), _ => return Err(GatewayClientError::ConnectionInInvalidState), - }; - - println!("registration result: {shared_key:?}"); + }?; let (authentication_status, gateway_protocol) = match self.read_control_response().await? { ServerResponse::Register { @@ -450,54 +446,48 @@ impl GatewayClient { self.check_gateway_protocol(gateway_protocol)?; self.authenticated = authentication_status; - todo!() - // if self.authenticated { - // self.shared_key = Some(Arc::new(shared_key)); - // } - // - // // populate the negotiated protocol for future uses - // self.negotiated_protocol = gateway_protocol; - // - // Ok(()) + if self.authenticated { + self.shared_key = Some(Arc::new(shared_key)); + } + + // populate the negotiated protocol for future uses + self.negotiated_protocol = gateway_protocol; + + Ok(()) } - async fn authenticate( - &mut self, - shared_key: Option, - supports_aes_gcm_siv: bool, - ) -> Result<(), GatewayClientError> { - if shared_key.is_none() && self.shared_key.is_none() { + async fn upgrade_key_authenticated(&mut self) -> Result<(), GatewayClientError> { + let Some(shared_key) = self.shared_key.as_ref() else { return Err(GatewayClientError::NoSharedKeyAvailable); - } + }; + + assert!(shared_key.is_legacy()); + let legacy_key = shared_key.unwrap_legacy(); + + unimplemented!() + } + + async fn authenticate(&mut self) -> Result<(), GatewayClientError> { + let Some(shared_key) = self.shared_key.as_ref() else { + return Err(GatewayClientError::NoSharedKeyAvailable); + }; + if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } - log::debug!("Authenticating with gateway"); + debug!("authenticating with gateway"); - todo!("if using legacy key, check if we can upgrade"); - - // 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 - let mut rng = OsRng; - - // because of the previous check one of the unwraps MUST succeed - let shared_key = shared_key - .as_ref() - .unwrap_or_else(|| self.shared_key.as_ref().unwrap()); - let iv = IV::new_random(&mut rng); let self_address = self .local_identity .as_ref() .public_key() .derive_destination_address(); - let encrypted_address = EncryptedAddressBytes::new(&self_address, shared_key, &iv); let msg = ClientControlRequest::new_authenticate( self_address, - encrypted_address, - iv, + shared_key, self.cfg.bandwidth.require_tickets, - ); + )?; match self.send_websocket_message(msg).await? { ServerResponse::Authenticate { @@ -511,6 +501,7 @@ impl GatewayClient { self.negotiated_protocol = protocol_version; log::debug!("authenticated: {status}, bandwidth remaining: {bandwidth_remaining}"); + self.task_client.send_status_msg(Box::new( BandwidthStatusMessage::RemainingBandwidth(bandwidth_remaining), )); @@ -530,7 +521,7 @@ impl GatewayClient { )] pub async fn perform_initial_authentication( &mut self, - ) -> Result, GatewayClientError> { + ) -> Result, GatewayClientError> { // 1. check gateway's protocol version let supports_aes_gcm_siv = match self.get_gateway_protocol().await { Ok(protocol) => protocol >= AES_GCM_SIV_PROTOCOL_VERSION, @@ -546,11 +537,6 @@ impl GatewayClient { warn!("this gateway is on an old version that doesn't support AES256-GCM-SIV"); } - // 2. if error or new handshake unsupported => fallback to the old registration/authentication - // 3. otherwise continue with the updated key derivation - - // ?. if new protocol is supported and we have an old key, upgrade it to aes-gcm-siv - if self.authenticated { debug!("Already authenticated"); return if let Some(shared_key) = &self.shared_key { @@ -561,10 +547,25 @@ impl GatewayClient { } if self.shared_key.is_some() { - self.authenticate(None, supports_aes_gcm_siv).await?; + self.authenticate().await?; + + if self.authenticated { + // if we managed to authenticate with an old key, see if we can upgrade it + if self + .shared_key + .as_ref() + .map(|k| k.is_legacy()) + .unwrap_or_default() + && supports_aes_gcm_siv + { + info!("using a legacy shared key and the gateway supports the updated format - attempting to upgrade"); + self.upgrade_key_authenticated().await?; + } + } } else { self.register(supports_aes_gcm_siv).await?; } + if self.authenticated { // if we are authenticated it means we MUST have an associated shared_key Ok(Arc::clone(self.shared_key.as_ref().unwrap())) @@ -592,14 +593,10 @@ impl GatewayClient { &mut self, credential: CredentialSpendingData, ) -> Result<(), GatewayClientError> { - let mut rng = OsRng; - let iv = IV::new_random(&mut rng); - let msg = ClientControlRequest::new_enc_ecash_credential( credential, self.shared_key.as_ref().unwrap(), - iv, - ); + )?; let bandwidth_remaining = match self.send_websocket_message(msg).await? { ServerResponse::Bandwidth { available_total } => Ok(available_total), ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), @@ -720,10 +717,10 @@ impl GatewayClient { return Err(GatewayClientError::ConnectionNotEstablished); } - let messages: Vec<_> = packets + let messages: Result, _> = packets .into_iter() .map(|mix_packet| { - BinaryRequest::new_forward_request(mix_packet).into_ws_message( + BinaryRequest::ForwardSphinx { packet: mix_packet }.into_ws_message( self.shared_key .as_ref() .expect("no shared key present even though we're authenticated!"), @@ -732,7 +729,7 @@ impl GatewayClient { .collect(); if let Err(err) = self - .batch_send_websocket_messages_without_response(messages) + .batch_send_websocket_messages_without_response(messages?) .await { if err.is_closed_connection() && self.cfg.connection.should_reconnect_on_failure { @@ -796,11 +793,11 @@ impl GatewayClient { } // note: into_ws_message encrypts the requests and adds a MAC on it. Perhaps it should // be more explicit in the naming? - let msg = BinaryRequest::new_forward_request(mix_packet).into_ws_message( + let msg = BinaryRequest::ForwardSphinx { packet: mix_packet }.into_ws_message( self.shared_key .as_ref() .expect("no shared key present even though we're authenticated!"), - ); + )?; self.send_with_reconnection_on_failure(msg).await } @@ -877,7 +874,7 @@ impl GatewayClient { pub async fn authenticate_and_start( &mut self, - ) -> Result, GatewayClientError> + ) -> Result, GatewayClientError> where C: DkgQueryClient + Send + Sync, St: CredentialStorage, diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index 0e7757a46c..d796b6df71 100644 --- a/common/client-libs/gateway-client/src/error.rs +++ b/common/client-libs/gateway-client/src/error.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use nym_gateway_requests::registration::handshake::error::HandshakeError; -use nym_gateway_requests::SimpleGatewayRequestsError; +use nym_gateway_requests::{GatewayRequestsError, SimpleGatewayRequestsError}; use std::io; use thiserror::Error; use tungstenite::Error as WsError; @@ -21,6 +21,9 @@ pub enum GatewayClientError { #[error("gateway returned an error response: {0}")] TypedGatewayError(SimpleGatewayRequestsError), + #[error("request error: {0}")] + RequestError(#[from] GatewayRequestsError), + #[error("There was a network error: {0}")] NetworkError(#[from] WsError), diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index 319c98b23f..57ac4f2714 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -8,6 +8,7 @@ use tungstenite::{protocol::Message, Error as WsError}; pub use client::{config::GatewayClientConfig, GatewayClient, GatewayConfig}; pub use nym_gateway_requests::registration::handshake::LegacySharedKeys; +use nym_gateway_requests::registration::handshake::SharedGatewayKey; pub use packet_router::{ AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender, PacketRouter, @@ -45,11 +46,11 @@ pub(crate) fn cleanup_socket_messages( pub(crate) fn try_decrypt_binary_message( bin_msg: Vec, - shared_keys: &LegacySharedKeys, + shared_keys: &SharedGatewayKey, ) -> Option> { match BinaryResponse::try_from_encrypted_tagged_bytes(bin_msg, shared_keys) { Ok(bin_response) => match bin_response { - BinaryResponse::PushedMixMessage(plaintext) => Some(plaintext), + BinaryResponse::PushedMixMessage { message } => Some(message), }, Err(err) => { warn!("message received from the gateway was malformed! - {err}",); diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index 46d81cb895..b32f85d3f3 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -9,7 +9,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::registration::handshake::LegacySharedKeys; +use nym_gateway_requests::registration::handshake::SharedGatewayKey; use nym_gateway_requests::{ServerResponse, SimpleGatewayRequestsError}; use nym_task::TaskClient; use si_scale::helpers::bibytes2; @@ -62,7 +62,7 @@ pub(crate) struct PartiallyDelegatedHandle { struct PartiallyDelegatedRouter { packet_router: PacketRouter, - shared_key: Arc, + shared_key: Arc, client_bandwidth: ClientBandwidth, stream_return: SplitStreamSender, @@ -72,7 +72,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<()>, @@ -247,7 +247,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: TaskClient, ) -> Self { diff --git a/common/gateway-requests/Cargo.toml b/common/gateway-requests/Cargo.toml index 3517df78b2..2b8a106794 100644 --- a/common/gateway-requests/Cargo.toml +++ b/common/gateway-requests/Cargo.toml @@ -17,6 +17,7 @@ generic-array = { workspace = true, features = ["serde"] } rand = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +strum = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true, features = ["log"] } zeroize = { workspace = true } diff --git a/common/gateway-requests/src/authentication/encrypted_address.rs b/common/gateway-requests/src/authentication/encrypted_address.rs index f061f170a4..f4a2f3e3a8 100644 --- a/common/gateway-requests/src/authentication/encrypted_address.rs +++ b/common/gateway-requests/src/authentication/encrypted_address.rs @@ -1,60 +1,47 @@ -// Copyright 2020 - Nym Technologies SA +// Copyright 2020-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::iv::IV; -use crate::registration::handshake::LegacySharedKeys; -use nym_crypto::symmetric::stream_cipher; -use nym_sphinx::params::LegacyGatewayEncryptionAlgorithm; -use nym_sphinx::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH}; +use crate::registration::handshake::{SharedGatewayKey, SharedKeyUsageError}; +use nym_sphinx::DestinationAddressBytes; use thiserror::Error; -pub const ENCRYPTED_ADDRESS_SIZE: usize = DESTINATION_ADDRESS_LENGTH; - /// 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, Copy)] -pub struct EncryptedAddressBytes([u8; ENCRYPTED_ADDRESS_SIZE]); +#[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); #[derive(Debug, Error)] pub enum EncryptedAddressConversionError { #[error("Failed to decode the encrypted address - {0}")] DecodeError(#[from] bs58::decode::Error), - #[error("The decoded address has invalid length")] - StringOfInvalidLengthError, } impl EncryptedAddressBytes { - pub fn new(address: &DestinationAddressBytes, key: &LegacySharedKeys, iv: &IV) -> Self { - let ciphertext = stream_cipher::encrypt::( - key.encryption_key(), - iv.inner(), - address.as_bytes_ref(), - ); + pub fn new( + address: &DestinationAddressBytes, + key: &SharedGatewayKey, + nonce: &[u8], + ) -> Result { + let ciphertext = key.encrypt_naive(address.as_bytes_ref(), Some(nonce))?; - let mut enc_address = [0u8; ENCRYPTED_ADDRESS_SIZE]; - enc_address.copy_from_slice(&ciphertext[..]); - EncryptedAddressBytes(enc_address) + Ok(EncryptedAddressBytes(ciphertext)) } pub fn verify( &self, address: &DestinationAddressBytes, - key: &LegacySharedKeys, - iv: &IV, + key: &SharedGatewayKey, + nonce: &[u8], ) -> bool { - self == &Self::new(address, key, iv) - } - - pub fn from_bytes(bytes: [u8; ENCRYPTED_ADDRESS_SIZE]) -> Self { - EncryptedAddressBytes(bytes) - } - - pub fn to_bytes(self) -> [u8; ENCRYPTED_ADDRESS_SIZE] { - self.0 + let Ok(reconstructed) = Self::new(address, key, nonce) else { + return false; + }; + self == &reconstructed } pub fn as_bytes(&self) -> &[u8] { @@ -65,14 +52,7 @@ impl EncryptedAddressBytes { val: S, ) -> Result { let decoded = bs58::decode(val.into()).into_vec()?; - - if decoded.len() != ENCRYPTED_ADDRESS_SIZE { - return Err(EncryptedAddressConversionError::StringOfInvalidLengthError); - } - - let mut enc_address = [0u8; ENCRYPTED_ADDRESS_SIZE]; - enc_address.copy_from_slice(&decoded[..]); - Ok(EncryptedAddressBytes(enc_address)) + Ok(EncryptedAddressBytes(decoded)) } pub fn to_base58_string(self) -> String { diff --git a/common/gateway-requests/src/iv.rs b/common/gateway-requests/src/iv.rs deleted file mode 100644 index 5db5b320bf..0000000000 --- a/common/gateway-requests/src/iv.rs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2020 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use nym_crypto::generic_array::{typenum::Unsigned, GenericArray}; -use nym_crypto::symmetric::stream_cipher::{random_iv, IvSizeUser, IV as CryptoIV}; -use nym_sphinx::params::LegacyGatewayEncryptionAlgorithm; -use rand::{CryptoRng, RngCore}; -use thiserror::Error; - -type NonceSize = ::IvSize; - -// I think 'IV' looks better than 'Iv', feel free to change that. -#[allow(clippy::upper_case_acronyms)] -pub struct IV(CryptoIV); - -#[derive(Error, Debug)] -// I think 'IV' looks better than 'Iv', feel free to change that. -#[allow(clippy::upper_case_acronyms)] -pub enum IVConversionError { - #[error("Failed to decode the iv - {0}")] - DecodeError(#[from] bs58::decode::Error), - - #[error("The decoded bytes iv has invalid length")] - BytesOfInvalidLengthError, - - #[error("The decoded string iv has invalid length")] - StringOfInvalidLengthError, -} - -impl IV { - pub fn new_random(rng: &mut R) -> Self { - IV(random_iv::(rng)) - } - - pub fn try_from_bytes(bytes: &[u8]) -> Result { - if bytes.len() != NonceSize::to_usize() { - return Err(IVConversionError::BytesOfInvalidLengthError); - } - - Ok(IV(GenericArray::clone_from_slice(bytes))) - } - - pub fn to_bytes(&self) -> Vec { - self.0.to_vec() - } - - pub fn as_bytes(&self) -> &[u8] { - self.0.as_ref() - } - - pub fn inner(&self) -> &CryptoIV { - &self.0 - } - - pub fn try_from_base58_string>(val: S) -> Result { - let decoded = bs58::decode(val.into()).into_vec()?; - - if decoded.len() != NonceSize::to_usize() { - return Err(IVConversionError::StringOfInvalidLengthError); - } - - Ok(IV( - GenericArray::from_exact_iter(decoded).expect("Invalid vector length!") - )) - } - - pub fn to_base58_string(&self) -> String { - bs58::encode(self.to_bytes()).into_string() - } -} - -impl From for String { - fn from(iv: IV) -> Self { - iv.to_base58_string() - } -} diff --git a/common/gateway-requests/src/lib.rs b/common/gateway-requests/src/lib.rs index 47e30e2758..4b83be8c10 100644 --- a/common/gateway-requests/src/lib.rs +++ b/common/gateway-requests/src/lib.rs @@ -2,13 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 pub use nym_crypto::generic_array; -use nym_crypto::hmac::HmacOutput; use nym_crypto::OutputSizeUser; use nym_sphinx::params::GatewayIntegrityHmacAlgorithm; + pub use types::*; pub mod authentication; -pub mod iv; pub mod models; pub mod registration; pub mod types; @@ -25,8 +24,6 @@ pub const INITIAL_PROTOCOL_VERSION: u8 = 1; pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: u8 = 2; pub const AES_GCM_SIV_PROTOCOL_VERSION: u8 = 3; -pub type GatewayMac = HmacOutput; - // TODO: could using `Mac` trait here for OutputSize backfire? // Should hmac itself be exposed, imported and used instead? -pub type GatewayMacSize = ::OutputSize; +pub type LegacyGatewayMacSize = ::OutputSize; diff --git a/common/gateway-requests/src/registration/handshake/client.rs b/common/gateway-requests/src/registration/handshake/client.rs index 0acb6bff43..aebda07551 100644 --- a/common/gateway-requests/src/registration/handshake/client.rs +++ b/common/gateway-requests/src/registration/handshake/client.rs @@ -6,7 +6,6 @@ use crate::registration::handshake::state::State; use crate::registration::handshake::SharedGatewayKey; use crate::registration::handshake::{error::HandshakeError, WsItem}; use futures::{Sink, Stream}; -use tracing::info; use tungstenite::Message as WsMessage; impl<'a, S> State<'a, S> { diff --git a/common/gateway-requests/src/registration/handshake/shared_key/legacy.rs b/common/gateway-requests/src/registration/handshake/shared_key/legacy.rs index 7b60a737b1..3d6bb905f0 100644 --- a/common/gateway-requests/src/registration/handshake/shared_key/legacy.rs +++ b/common/gateway-requests/src/registration/handshake/shared_key/legacy.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::registration::handshake::shared_key::{SharedKeyConversionError, SharedKeyUsageError}; -use crate::GatewayMacSize; +use crate::LegacyGatewayMacSize; use nym_crypto::generic_array::{ typenum::{Sum, Unsigned, U16}, GenericArray, @@ -110,7 +110,7 @@ impl LegacySharedKeys { enc_data: &[u8], iv: Option<&IV>, ) -> Result, SharedKeyUsageError> { - let mac_size = GatewayMacSize::to_usize(); + let mac_size = LegacyGatewayMacSize::to_usize(); if enc_data.len() < mac_size { return Err(SharedKeyUsageError::TooShortRequest); } @@ -128,16 +128,16 @@ impl LegacySharedKeys { // couldn't have made the first borrow mutable as you can't have an immutable borrow // together with a mutable one - let message_bytes_mut = &mut enc_data.to_vec()[mac_size..]; + 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, - message_bytes_mut, + &mut message_bytes_mut, ); - Ok(message_bytes_mut.to_vec()) + Ok(message_bytes_mut) } pub fn encryption_key(&self) -> &CipherKey { diff --git a/common/gateway-requests/src/registration/handshake/shared_key/mod.rs b/common/gateway-requests/src/registration/handshake/shared_key/mod.rs index b199e09752..4fee47b6cc 100644 --- a/common/gateway-requests/src/registration/handshake/shared_key/mod.rs +++ b/common/gateway-requests/src/registration/handshake/shared_key/mod.rs @@ -3,10 +3,13 @@ use crate::registration::handshake::LegacySharedKeys; use nym_crypto::generic_array::{typenum::Unsigned, GenericArray}; -use nym_crypto::symmetric::aead::{self, nonce_size, AeadError, AeadKey, KeySizeUser, Nonce}; -use nym_crypto::symmetric::stream_cipher::{iv_size, IV}; +use nym_crypto::symmetric::aead::{ + self, nonce_size, random_nonce, AeadError, AeadKey, KeySizeUser, Nonce, +}; +use nym_crypto::symmetric::stream_cipher::{iv_size, random_iv, IV}; use nym_pemstore::traits::PemStorableKey; use nym_sphinx::params::{GatewayEncryptionAlgorithm, LegacyGatewayEncryptionAlgorithm}; +use rand::thread_rng; use serde::{Deserialize, Serialize}; use thiserror::Error; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; @@ -15,12 +18,77 @@ pub mod legacy; pub type SharedKeySize = ::KeySize; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Zeroize)] 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, + } + } + + 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")] diff --git a/common/gateway-requests/src/registration/handshake/state.rs b/common/gateway-requests/src/registration/handshake/state.rs index 0522ae6631..c1b5e1056a 100644 --- a/common/gateway-requests/src/registration/handshake/state.rs +++ b/common/gateway-requests/src/registration/handshake/state.rs @@ -170,8 +170,6 @@ impl<'a, S> State<'a, S> { .collect(); let signature = self.identity.private_key().sign(plaintext); - println!("signature len: {}", signature.to_bytes().len()); - let nonce = if self.derive_aes256_gcm_siv_key { let mut rng = thread_rng(); Some(random_nonce::(&mut rng).to_vec()) @@ -179,8 +177,6 @@ impl<'a, S> State<'a, S> { None }; - println!("key types: {:?}", self.derived_shared_keys); - // SAFETY: this function is only called after the local key has already been derived let signature_ciphertext = self .derived_shared_keys diff --git a/common/gateway-requests/src/types.rs b/common/gateway-requests/src/types.rs index 82dab71853..b223502b39 100644 --- a/common/gateway-requests/src/types.rs +++ b/common/gateway-requests/src/types.rs @@ -1,24 +1,22 @@ // Copyright 2020-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::authentication::encrypted_address::EncryptedAddressBytes; -use crate::iv::{IVConversionError, IV}; use crate::models::CredentialSpendingRequest; -use crate::registration::handshake::{LegacySharedKeys, SharedKeyUsageError}; -use crate::{GatewayMacSize, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION}; +use crate::registration::handshake::{SharedGatewayKey, SharedKeyUsageError}; +use crate::{ + AES_GCM_SIV_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, +}; use nym_credentials::ecash::bandwidth::CredentialSpendingData; use nym_credentials_interface::CompactEcashError; -use nym_crypto::generic_array::typenum::Unsigned; -use nym_crypto::hmac::recompute_keyed_hmac_and_verify_tag; -use nym_crypto::symmetric::stream_cipher; use nym_sphinx::addressing::nodes::NymNodeRoutingAddressError; use nym_sphinx::forwarding::packet::{MixPacket, MixPacketFormattingError}; use nym_sphinx::params::packet_sizes::PacketSize; -use nym_sphinx::params::{GatewayIntegrityHmacAlgorithm, LegacyGatewayEncryptionAlgorithm}; use nym_sphinx::DestinationAddressBytes; use serde::{Deserialize, Serialize}; +use std::iter::once; use std::str::FromStr; use std::string::FromUtf8Error; +use strum::FromRepr; use thiserror::Error; use tracing::log::error; use tungstenite::protocol::Message; @@ -97,15 +95,21 @@ pub enum GatewayRequestsError { #[error(transparent)] KeyUsageFailure(#[from] SharedKeyUsageError), + #[error("received request with an unknown kind: {kind}")] + UnknownRequestKind { kind: u8 }, + + #[error("received response with an unknown kind: {kind}")] + UnknownResponseKind { kind: u8 }, + + #[error("the encryption flag had an unexpected value")] + InvalidEncryptionFlag, + #[error("the request is too short")] TooShortRequest, #[error("provided MAC is invalid")] InvalidMac, - #[error("Provided bandwidth IV is malformed: {0}")] - MalformedIV(#[from] IVConversionError), - #[error("address field was incorrectly encoded: {source}")] IncorrectlyEncodedAddress { #[from] @@ -124,18 +128,15 @@ pub enum GatewayRequestsError { #[error("received sphinx packet was malformed")] MalformedSphinxPacket, + #[error("failed to serialise created sphinx packet: {0}")] + SphinxSerialisationFailure(#[from] MixPacketFormattingError), + #[error("the received encrypted data was malformed")] MalformedEncryption, #[error("provided packet mode is invalid")] InvalidPacketMode, - #[error("provided mix packet was malformed: {source}")] - InvalidMixPacket { - #[from] - source: MixPacketFormattingError, - }, - #[error("failed to deserialize provided credential: {0}")] EcashCredentialDeserializationFailure(#[from] CompactEcashError), @@ -190,24 +191,29 @@ pub enum ClientControlRequest { impl ClientControlRequest { pub fn new_authenticate( address: DestinationAddressBytes, - enc_address: EncryptedAddressBytes, - iv: IV, + shared_key: &SharedGatewayKey, uses_credentials: bool, - ) -> Self { - // if we're not going to be using credentials, advertise lower protocol version to allow connection - // to wider range of gateways - let protocol_version = if uses_credentials { - Some(CURRENT_PROTOCOL_VERSION) + ) -> Result { + // if we're encrypting with non-legacy key, the remote must support AES256-GCM-SIV + 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) }; - ClientControlRequest::Authenticate { + 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: enc_address.to_base58_string(), - iv: iv.to_base58_string(), - } + enc_address: bs58::encode(&ciphertext).into_string(), + iv: bs58::encode(&nonce).into_string(), + }) } pub fn name(&self) -> String { @@ -230,26 +236,26 @@ impl ClientControlRequest { pub fn new_enc_ecash_credential( credential: CredentialSpendingData, - shared_key: &LegacySharedKeys, - iv: IV, - ) -> Self { + shared_key: &SharedGatewayKey, + ) -> Result { let cred = CredentialSpendingRequest::new(credential); let serialized_credential = cred.to_bytes(); - let enc_credential = shared_key.encrypt_and_tag(&serialized_credential, Some(iv.inner())); - ClientControlRequest::EcashCredential { + let nonce = shared_key.random_nonce_or_iv(); + let enc_credential = shared_key.encrypt(&serialized_credential, Some(&nonce))?; + + Ok(ClientControlRequest::EcashCredential { enc_credential, - iv: iv.to_bytes(), - } + iv: nonce, + }) } pub fn try_from_enc_ecash_credential( enc_credential: Vec, - shared_key: &LegacySharedKeys, + shared_key: &SharedGatewayKey, iv: Vec, ) -> Result { - let iv = IV::try_from_bytes(&iv)?; - let credential_bytes = shared_key.decrypt_tagged(&enc_credential, Some(iv.inner()))?; + let credential_bytes = shared_key.decrypt(&enc_credential, Some(&iv))?; CredentialSpendingRequest::try_from_bytes(credential_bytes.as_slice()) .map_err(|_| GatewayRequestsError::MalformedEncryption) } @@ -370,8 +376,141 @@ impl TryFrom for ServerResponse { } } +// each binary message consists of the following structure (for non-legacy messages) +// KIND || ENC_FLAG || MAYBE_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 +// finally, the rest of the message is the associated ciphertext or just plaintext (if message wasn't encrypted) +pub struct BinaryData<'a> { + kind: u8, + encrypted: bool, + maybe_nonce: Option<&'a [u8]>, + data: &'a [u8], +} + +impl<'a> BinaryData<'a> { + // serialises possibly encrypted data into bytes to be put on the wire + pub fn into_raw(self, legacy: bool) -> Vec { + if legacy { + return self.data.to_vec(); + } + + let i = once(self.kind).chain(once(if self.encrypted { 1 } else { 0 })); + if let Some(nonce) = self.maybe_nonce { + i.chain(nonce.iter().copied()) + .chain(self.data.iter().copied()) + .collect() + } else { + i.chain(self.data.iter().copied()).collect() + } + } + + // attempts to perform basic parsing on bytes received from the wire + pub fn from_raw( + raw: &'a [u8], + available_key: &SharedGatewayKey, + ) -> 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); + } + + let kind = raw[0]; + let encrypted = if raw[1] == 1 { + true + } else if raw[1] == 0 { + false + } else { + return Err(GatewayRequestsError::InvalidEncryptionFlag); + }; + + // if data is encrypted, there MUST be a nonce present for non-legacy keys + if encrypted && raw.len() < available_key.nonce_size() + 2 { + return Err(GatewayRequestsError::TooShortRequest); + } + + Ok(BinaryData { + kind, + encrypted, + maybe_nonce: Some(&raw[2..2 + available_key.nonce_size()]), + data: &raw[2 + available_key.nonce_size()..], + }) + } + + // attempt to encrypt plaintext of provided response/request and serialise it into wire format + pub fn make_encrypted_blob( + kind: u8, + plaintext: &[u8], + key: &SharedGatewayKey, + ) -> Result, GatewayRequestsError> { + let maybe_nonce = key.random_nonce_or_zero_iv(); + + let ciphertext = key.encrypt(plaintext, maybe_nonce.as_deref())?; + Ok(BinaryData { + kind, + encrypted: true, + maybe_nonce: maybe_nonce.as_deref(), + data: &ciphertext, + } + .into_raw(key.is_legacy())) + } + + // attempts to parse previously recovered bytes into a [`BinaryRequest`] + pub fn into_request( + self, + key: &SharedGatewayKey, + ) -> 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)? + } else { + self.data + }; + + BinaryRequest::from_plaintext(kind, plaintext) + } + + // attempts to parse previously recovered bytes into a [`BinaryResponse`] + pub fn into_response( + self, + key: &SharedGatewayKey, + ) -> 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)? + } else { + self.data + }; + + BinaryResponse::from_plaintext(kind, plaintext) + } +} + +// in legacy mode requests use zero IV without pub enum BinaryRequest { - ForwardSphinx(MixPacket), + ForwardSphinx { packet: MixPacket }, +} + +#[repr(u8)] +#[derive(Debug, Clone, Copy, FromRepr, PartialEq)] +#[non_exhaustive] +pub enum BinaryRequestKind { + ForwardSphinx = 1, } // Right now the only valid `BinaryRequest` is a request to forward a sphinx packet. @@ -380,96 +519,118 @@ pub enum BinaryRequest { // HOWEVER, NOTE: If we introduced another 'BinaryRequest', we must carefully examine if a 0s IV // would work there. impl BinaryRequest { - pub fn try_from_encrypted_tagged_bytes( - raw_req: Vec, - shared_keys: &LegacySharedKeys, - ) -> Result { - let message_bytes = &shared_keys.decrypt_tagged(&raw_req, None)?; - - // right now there's only a single option possible which significantly simplifies the logic - // if we decided to allow for more 'binary' messages, the API wouldn't need to change. - let mix_packet = MixPacket::try_from_bytes(message_bytes)?; - Ok(BinaryRequest::ForwardSphinx(mix_packet)) + pub fn kind(&self) -> BinaryRequestKind { + match self { + BinaryRequest::ForwardSphinx { .. } => BinaryRequestKind::ForwardSphinx, + } } - pub fn into_encrypted_tagged_bytes(self, shared_key: &LegacySharedKeys) -> Vec { - match self { - BinaryRequest::ForwardSphinx(mix_packet) => { - let forwarding_data = match mix_packet.into_bytes() { - Ok(mix_packet) => mix_packet, - Err(e) => { - error!("Could not convert packet to bytes: {e}"); - return vec![]; - } - }; - - // TODO: it could be theoretically slightly more efficient if the data wasn't taken - // by reference because then it makes a copy for encryption rather than do it in place - shared_key.encrypt_and_tag(&forwarding_data, None) + pub fn from_plaintext( + kind: BinaryRequestKind, + plaintext: &[u8], + ) -> Result { + match kind { + BinaryRequestKind::ForwardSphinx => { + let packet = MixPacket::try_from_bytes(plaintext)?; + Ok(BinaryRequest::ForwardSphinx { packet }) } } } - // TODO: this will be encrypted, etc. - pub fn new_forward_request(mix_packet: MixPacket) -> BinaryRequest { - BinaryRequest::ForwardSphinx(mix_packet) + pub fn try_from_encrypted_tagged_bytes( + bytes: Vec, + shared_key: &SharedGatewayKey, + ) -> Result { + BinaryData::from_raw(&bytes, shared_key)?.into_request(shared_key) } - pub fn into_ws_message(self, shared_key: &LegacySharedKeys) -> Message { - Message::Binary(self.into_encrypted_tagged_bytes(shared_key)) + pub fn into_encrypted_tagged_bytes( + self, + shared_key: &SharedGatewayKey, + ) -> Result, GatewayRequestsError> { + let kind = self.kind(); + + let plaintext = match self { + BinaryRequest::ForwardSphinx { packet } => packet.into_bytes()?, + }; + + BinaryData::make_encrypted_blob(kind as u8, &plaintext, shared_key) + } + + pub fn into_ws_message( + self, + shared_key: &SharedGatewayKey, + ) -> Result { + // all variants are currently encrypted + let blob = match self { + BinaryRequest::ForwardSphinx { .. } => self.into_encrypted_tagged_bytes(shared_key)?, + }; + + Ok(Message::Binary(blob)) } } -// Introduced for consistency sake pub enum BinaryResponse { - PushedMixMessage(Vec), + PushedMixMessage { message: Vec }, +} + +#[repr(u8)] +#[derive(Debug, Clone, Copy, FromRepr, PartialEq)] +#[non_exhaustive] +pub enum BinaryResponseKind { + PushedMixMessage = 1, } impl BinaryResponse { - pub fn try_from_encrypted_tagged_bytes( - raw_req: Vec, - shared_keys: &LegacySharedKeys, - ) -> Result { - let mac_size = GatewayMacSize::to_usize(); - if raw_req.len() < mac_size { - return Err(GatewayRequestsError::TooShortRequest); + pub fn kind(&self) -> BinaryResponseKind { + match self { + BinaryResponse::PushedMixMessage { .. } => BinaryResponseKind::PushedMixMessage, } + } - let mac_tag = &raw_req[..mac_size]; - let message_bytes = &raw_req[mac_size..]; - - if !recompute_keyed_hmac_and_verify_tag::( - shared_keys.mac_key().as_slice(), - message_bytes, - mac_tag, - ) { - return Err(GatewayRequestsError::InvalidMac); + pub fn from_plaintext( + kind: BinaryResponseKind, + plaintext: &[u8], + ) -> Result { + match kind { + BinaryResponseKind::PushedMixMessage => Ok(BinaryResponse::PushedMixMessage { + message: plaintext.to_vec(), + }), } - - let zero_iv = stream_cipher::zero_iv::(); - let plaintext = stream_cipher::decrypt::( - shared_keys.encryption_key(), - &zero_iv, - message_bytes, - ); - - Ok(BinaryResponse::PushedMixMessage(plaintext)) } - pub fn into_encrypted_tagged_bytes(self, shared_key: &LegacySharedKeys) -> Vec { - match self { - // TODO: it could be theoretically slightly more efficient if the data wasn't taken - // by reference because then it makes a copy for encryption rather than do it in place - BinaryResponse::PushedMixMessage(message) => shared_key.encrypt_and_tag(&message, None), - } + pub fn try_from_encrypted_tagged_bytes( + bytes: Vec, + shared_key: &SharedGatewayKey, + ) -> Result { + BinaryData::from_raw(&bytes, shared_key)?.into_response(shared_key) } - pub fn new_pushed_mix_message(msg: Vec) -> Self { - BinaryResponse::PushedMixMessage(msg) + pub fn into_encrypted_tagged_bytes( + self, + shared_key: &SharedGatewayKey, + ) -> Result, GatewayRequestsError> { + let kind = self.kind(); + + let plaintext = match self { + BinaryResponse::PushedMixMessage { message } => message, + }; + + BinaryData::make_encrypted_blob(kind as u8, &plaintext, shared_key) } - pub fn into_ws_message(self, shared_key: &LegacySharedKeys) -> Message { - Message::Binary(self.into_encrypted_tagged_bytes(shared_key)) + pub fn into_ws_message( + self, + shared_key: &SharedGatewayKey, + ) -> Result { + // all variants are currently encrypted + let blob = match self { + BinaryResponse::PushedMixMessage { .. } => { + self.into_encrypted_tagged_bytes(shared_key)? + } + }; + + Ok(Message::Binary(blob)) } } diff --git a/common/gateway-storage/Cargo.toml b/common/gateway-storage/Cargo.toml index 26b2be56a1..d89bac7639 100644 --- a/common/gateway-storage/Cargo.toml +++ b/common/gateway-storage/Cargo.toml @@ -19,6 +19,7 @@ sqlx = { workspace = true, features = [ "macros", "migrate", "time", + "chrono" ] } time = { workspace = true } thiserror = { workspace = true } diff --git a/common/gateway-storage/migrations/20240916120000_add_aes256_gcm_siv_key.sql b/common/gateway-storage/migrations/20240916120000_add_aes256_gcm_siv_key.sql new file mode 100644 index 0000000000..6327832639 --- /dev/null +++ b/common/gateway-storage/migrations/20240916120000_add_aes256_gcm_siv_key.sql @@ -0,0 +1,13 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +-- make aes128 key column nullable and add aes256 column +ALTER TABLE shared_keys RENAME COLUMN derived_aes128_ctr_blake3_hmac_keys_bs58 TO derived_aes128_ctr_blake3_hmac_keys_bs58_old; +ALTER TABLE shared_keys ADD COLUMN derived_aes128_ctr_blake3_hmac_keys_bs58 TEXT; +ALTER TABLE shared_keys ADD COLUMN derived_aes256_gcm_siv_key BLOB; + +UPDATE shared_keys SET derived_aes128_ctr_blake3_hmac_keys_bs58 = derived_aes128_ctr_blake3_hmac_keys_bs58_old; + +ALTER TABLE shared_keys DROP COLUMN derived_aes128_ctr_blake3_hmac_keys_bs58_old; diff --git a/common/gateway-storage/src/error.rs b/common/gateway-storage/src/error.rs index 178d630c10..408ec245d6 100644 --- a/common/gateway-storage/src/error.rs +++ b/common/gateway-storage/src/error.rs @@ -11,6 +11,9 @@ pub enum StorageError { #[error("Failed to perform database migration: {0}")] MigrationError(#[from] sqlx::migrate::MigrateError), + #[error("could not find any valid shared keys for under id {id}")] + MissingSharedKey { id: i64 }, + #[error("Somehow stored data is incorrect: {0}")] DataCorruption(String), diff --git a/common/gateway-storage/src/lib.rs b/common/gateway-storage/src/lib.rs index 24fd0ee7b1..4bae9568aa 100644 --- a/common/gateway-storage/src/lib.rs +++ b/common/gateway-storage/src/lib.rs @@ -11,7 +11,7 @@ use models::{ VerifiedTicket, WireguardPeer, }; use nym_credentials_interface::ClientTicket; -use nym_gateway_requests::registration::handshake::LegacySharedKeys; +use nym_gateway_requests::registration::handshake::SharedGatewayKey; use nym_sphinx::DestinationAddressBytes; use shared_keys::SharedKeysManager; use sqlx::ConnectOptions; @@ -42,11 +42,13 @@ pub trait Storage: Send + Sync { /// # Arguments /// /// * `client_address`: base58-encoded address of the client - /// * `shared_keys`: shared encryption (AES128CTR) and mac (hmac-blake3) derived shared keys to store. + /// * `shared_keys`: + /// - legacy: shared encryption (AES128CTR) and mac (hmac-blake3) derived shared keys to store. + /// - current: shared AES256-GCM-SIV keys async fn insert_shared_keys( &self, client_address: DestinationAddressBytes, - shared_keys: &LegacySharedKeys, + shared_keys: &SharedGatewayKey, ) -> Result; /// Tries to retrieve shared keys stored for the particular client. @@ -330,7 +332,7 @@ impl Storage for PersistentStorage { async fn insert_shared_keys( &self, client_address: DestinationAddressBytes, - shared_keys: &LegacySharedKeys, + shared_keys: &SharedGatewayKey, ) -> Result { let client_id = self .client_manager @@ -340,7 +342,8 @@ impl Storage for PersistentStorage { .insert_shared_keys( client_id, client_address.as_base58_string(), - shared_keys.to_base58_string(), + shared_keys.aes128_ctr_hmac_bs58().as_deref(), + shared_keys.aes256_gcm_siv().as_deref(), ) .await?; Ok(client_id) diff --git a/common/gateway-storage/src/models.rs b/common/gateway-storage/src/models.rs index 09df516e9b..4b6afef89a 100644 --- a/common/gateway-storage/src/models.rs +++ b/common/gateway-storage/src/models.rs @@ -3,6 +3,9 @@ use crate::error::StorageError; use nym_credentials_interface::{AvailableBandwidth, ClientTicket, CredentialSpendingData}; +use nym_gateway_requests::registration::handshake::{ + LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey, +}; use sqlx::FromRow; use time::OffsetDateTime; @@ -11,13 +14,38 @@ pub struct Client { pub client_type: crate::clients::ClientType, } +#[derive(FromRow)] pub struct PersistedSharedKeys { #[allow(dead_code)] pub client_id: i64, #[allow(dead_code)] pub client_address_bs58: String, - pub derived_aes128_ctr_blake3_hmac_keys_bs58: String, + pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option, + pub derived_aes256_gcm_siv_key: Option>, +} + +impl TryFrom for SharedGatewayKey { + type Error = StorageError; + + fn try_from(value: PersistedSharedKeys) -> Result { + match ( + &value.derived_aes256_gcm_siv_key, + &value.derived_aes128_ctr_blake3_hmac_keys_bs58, + ) { + (None, None) => Err(StorageError::MissingSharedKey { id: value.id }), + (Some(aes256gcm_siv), _) => { + let current_key = SharedSymmetricKey::try_from_bytes(aes256gcm_siv) + .map_err(|source| StorageError::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| StorageError::DataCorruption(source.to_string()))?; + Ok(SharedGatewayKey::Legacy(legacy_key)) + } + } + } } pub struct StoredMessage { diff --git a/common/gateway-storage/src/shared_keys.rs b/common/gateway-storage/src/shared_keys.rs index 4144dee0b7..ef7fac2eef 100644 --- a/common/gateway-storage/src/shared_keys.rs +++ b/common/gateway-storage/src/shared_keys.rs @@ -41,19 +41,27 @@ impl SharedKeysManager { &self, client_id: i64, client_address_bs58: String, - derived_aes128_ctr_blake3_hmac_keys_bs58: String, + derived_aes128_ctr_blake3_hmac_keys_bs58: Option<&String>, + derived_aes256_gcm_siv_key: Option<&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) VALUES (?, ?, ?); - UPDATE shared_keys SET derived_aes128_ctr_blake3_hmac_keys_bs58 = ? WHERE client_address_bs58 = ? + INSERT OR IGNORE INTO shared_keys(client_id, client_address_bs58, derived_aes128_ctr_blake3_hmac_keys_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/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs index 18efea6588..8c6cb2ef29 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -286,10 +286,9 @@ where } Ok(request) => match request { // currently only a single type exists - BinaryRequest::ForwardSphinx(mix_packet) => self - .handle_forward_sphinx(mix_packet) - .await - .into_ws_message(), + BinaryRequest::ForwardSphinx { packet } => { + self.handle_forward_sphinx(packet).await.into_ws_message() + } }, } } 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 7d2d41fa7d..34a313dd0d 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -21,12 +21,8 @@ use nym_crypto::asymmetric::identity; use nym_gateway_requests::authentication::encrypted_address::{ EncryptedAddressBytes, EncryptedAddressConversionError, }; -use nym_gateway_requests::registration::handshake::SharedGatewayKey; use nym_gateway_requests::{ - iv::{IVConversionError, IV}, - registration::handshake::{ - error::HandshakeError, gateway_handshake, LegacySharedKeys, SharedKeyConversionError, - }, + registration::handshake::{error::HandshakeError, gateway_handshake, SharedGatewayKey}, types::{ClientControlRequest, ServerResponse}, BinaryResponse, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, }; @@ -54,7 +50,7 @@ pub(crate) enum InitialAuthenticationError { MalformedStoredSharedKey { client_id: String, #[source] - source: SharedKeyConversionError, + source: StorageError, }, #[error("Failed to perform registration handshake: {0}")] @@ -70,8 +66,8 @@ pub(crate) enum InitialAuthenticationError { #[error("There is already an open connection to this client")] DuplicateConnection, - #[error("Provided authentication IV is malformed: {0}")] - MalformedIV(#[from] IVConversionError), + #[error("provided authentication IV is malformed: {0}")] + MalformedIV(bs58::decode::Error), #[error("Only 'Register' or 'Authenticate' requests are allowed")] InvalidRequest, @@ -260,7 +256,7 @@ where /// * `packets`: unwrapped packets that are to be pushed back to the client. pub(crate) async fn push_packets_to_client( &mut self, - shared_keys: &LegacySharedKeys, + shared_keys: &SharedGatewayKey, packets: Vec>, ) -> Result<(), WsError> where @@ -270,10 +266,13 @@ where // be more explicit in the naming? let messages: Vec> = packets .into_iter() - .map(|received_message| { - Ok(BinaryResponse::new_pushed_mix_message(received_message) - .into_ws_message(shared_keys)) + .filter_map(|message| { + BinaryResponse::PushedMixMessage { message } + .into_ws_message(shared_keys) + .inspect_err(|err| error!("failed to encrypt client message: {err}")) + .ok() }) + .map(|msg| Ok(msg)) .collect(); let mut send_stream = futures::stream::iter(messages); match self.socket_connection { @@ -315,7 +314,7 @@ where async fn push_stored_messages_to_client( &mut self, client_address: DestinationAddressBytes, - shared_keys: &LegacySharedKeys, + shared_keys: &SharedGatewayKey, ) -> Result<(), InitialAuthenticationError> where S: AsyncRead + AsyncWrite + Unpin, @@ -363,44 +362,33 @@ where /// /// * `client_address`: address of the client. /// * `encrypted_address`: encrypted address of the client, presumably encrypted using the shared keys. - /// * `iv`: iv created for this particular encryption. + /// * `iv`: nonce/iv created for this particular encryption. async fn verify_stored_shared_key( &self, client_address: DestinationAddressBytes, encrypted_address: EncryptedAddressBytes, - iv: IV, - ) -> Result, InitialAuthenticationError> { + nonce: &[u8], + ) -> Result, InitialAuthenticationError> { let shared_keys = self .shared_state .storage .get_shared_keys(client_address) .await?; - if let Some(shared_keys) = shared_keys { - // this should never fail as we only ever construct persisted shared keys ourselves when inserting - // data to the storage. The only way it could fail is if we somehow changed implementation without - // performing proper migration - let keys = LegacySharedKeys::try_from_base58_string( - shared_keys.derived_aes128_ctr_blake3_hmac_keys_bs58, - ) - .map_err(|source| { - InitialAuthenticationError::MalformedStoredSharedKey { - client_id: client_address.as_base58_string(), - source, - } - })?; - - // TODO: SECURITY: - // this is actually what we have been doing in the past, however, - // after looking deeper into implementation it seems that only checks the encryption - // key part of the shared keys. the MAC key might still be wrong - // (though I don't see how could this happen unless client messed with himself - // and I don't think it could lead to any attacks, but somebody smarter should take a look) - if encrypted_address.verify(&client_address, &keys, &iv) { - Ok(Some(keys)) - } else { - Ok(None) + let Some(stored_shared_keys) = shared_keys else { + return Ok(None); + }; + + let keys = SharedGatewayKey::try_from(stored_shared_keys).map_err(|source| { + InitialAuthenticationError::MalformedStoredSharedKey { + client_id: client_address.as_base58_string(), + source, } + })?; + + // LEGACY ISSUE: we're not verifying HMAC key + if encrypted_address.verify(&client_address, &keys, nonce) { + Ok(Some(keys)) } else { Ok(None) } @@ -452,13 +440,13 @@ where /// /// * `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. + /// * `iv`: fresh nonce/IV received with the request. async fn authenticate_client( &mut self, client_address: DestinationAddressBytes, encrypted_address: EncryptedAddressBytes, - iv: IV, - ) -> Result, InitialAuthenticationError> + nonce: &[u8], + ) -> Result, InitialAuthenticationError> where S: AsyncRead + AsyncWrite + Unpin, { @@ -468,7 +456,7 @@ where ); let shared_keys = self - .verify_stored_shared_key(client_address, encrypted_address, iv) + .verify_stored_shared_key(client_address, encrypted_address, nonce) .await?; if let Some(shared_keys) = shared_keys { @@ -550,7 +538,7 @@ where client_protocol_version: Option, address: String, enc_address: String, - iv: String, + raw_nonce: String, ) -> Result where S: AsyncRead + AsyncWrite + Unpin, @@ -564,7 +552,9 @@ where 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 iv = IV::try_from_base58_string(iv)?; + let nonce = bs58::decode(&raw_nonce) + .into_vec() + .map_err(InitialAuthenticationError::MalformedIV)?; // Check for duplicate clients if let Some(client_tx) = self.active_clients_store.get_remote_client(address) { @@ -574,7 +564,7 @@ where } let Some(shared_keys) = self - .authenticate_client(address, encrypted_address, iv) + .authenticate_client(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 @@ -623,7 +613,7 @@ where async fn register_client( &mut self, client_address: DestinationAddressBytes, - client_shared_keys: &LegacySharedKeys, + client_shared_keys: &SharedGatewayKey, ) -> Result where S: AsyncRead + AsyncWrite + Unpin, 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 fa78d8fe8b..bc9f47f4b8 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::registration::handshake::LegacySharedKeys; +use nym_gateway_requests::registration::handshake::SharedGatewayKey; use nym_gateway_requests::ServerResponse; use nym_gateway_storage::Storage; use nym_sphinx::DestinationAddressBytes; @@ -46,14 +46,14 @@ pub(crate) struct ClientDetails { #[zeroize(skip)] pub(crate) address: DestinationAddressBytes, pub(crate) id: i64, - pub(crate) shared_keys: LegacySharedKeys, + pub(crate) shared_keys: SharedGatewayKey, } impl ClientDetails { pub(crate) fn new( id: i64, address: DestinationAddressBytes, - shared_keys: LegacySharedKeys, + shared_keys: SharedGatewayKey, ) -> Self { ClientDetails { address, From c39d42b7ddd47adcb510edf0a845fb875c96e560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 16 Sep 2024 17:37:53 +0100 Subject: [PATCH 05/17] fixed deserialisation of updated gateway shared materials --- .../gateway-requests/src/registration/handshake/messages.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/common/gateway-requests/src/registration/handshake/messages.rs b/common/gateway-requests/src/registration/handshake/messages.rs index 0d3195aad5..d2f08708c1 100644 --- a/common/gateway-requests/src/registration/handshake/messages.rs +++ b/common/gateway-requests/src/registration/handshake/messages.rs @@ -177,9 +177,11 @@ impl HandshakeMessage for GatewayMaterialExchange { { // we expect to receive either: // LEGACY: x25519 pubkey + ed25519 signature ciphertext (96 bytes) - // CURRENT: x25519 pubkey + ed25519 signature ciphertext + AES256-GCM-SIV nonce (112 bytes) + // CURRENT: x25519 pubkey + ed25519 signature ciphertext (+ tag)+ AES256-GCM-SIV nonce (124 bytes) let legacy_len = x25519::PUBLIC_KEY_SIZE + ed25519::SIGNATURE_LENGTH; - let current_len = legacy_len + nonce_size::(); + let current_len = legacy_len + + nonce_size::() + + tag_size::(); if bytes.len() != legacy_len && bytes.len() != current_len { return Err(HandshakeError::MalformedResponse); From b6f07fbfce173f438aa70696248e43c9d9215381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 16 Sep 2024 17:38:01 +0100 Subject: [PATCH 06/17] warning for unimplemented upgrade --- common/client-libs/gateway-client/src/client/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index 6ed2b98ec9..6cb784cdbf 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -464,7 +464,10 @@ impl GatewayClient { assert!(shared_key.is_legacy()); let legacy_key = shared_key.unwrap_legacy(); - unimplemented!() + let _ = legacy_key; + warn!("unimplemented: migration into aes256gcm-siv key!"); + + Ok(()) } async fn authenticate(&mut self) -> Result<(), GatewayClientError> { From a65df5a0abfcc5486144eb42460731ec678a5ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 16 Sep 2024 17:39:00 +0100 Subject: [PATCH 07/17] clippy --- .../node/client_handling/websocket/connection_handler/fresh.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 34a313dd0d..8b96a5bda0 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -272,7 +272,7 @@ where .inspect_err(|err| error!("failed to encrypt client message: {err}")) .ok() }) - .map(|msg| Ok(msg)) + .map(Ok) .collect(); let mut send_stream = futures::stream::iter(messages); match self.socket_connection { From 114db3c1cf0ba7e8d0081e80dff872f056bfee21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 16 Sep 2024 17:42:16 +0100 Subject: [PATCH 08/17] post-rebasing fixes --- common/gateway-storage/src/models.rs | 2 +- common/gateway-storage/src/shared_keys.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/gateway-storage/src/models.rs b/common/gateway-storage/src/models.rs index 4b6afef89a..49fbc05761 100644 --- a/common/gateway-storage/src/models.rs +++ b/common/gateway-storage/src/models.rs @@ -33,7 +33,7 @@ impl TryFrom for SharedGatewayKey { &value.derived_aes256_gcm_siv_key, &value.derived_aes128_ctr_blake3_hmac_keys_bs58, ) { - (None, None) => Err(StorageError::MissingSharedKey { id: value.id }), + (None, None) => Err(StorageError::MissingSharedKey { id: value.client_id }), (Some(aes256gcm_siv), _) => { let current_key = SharedSymmetricKey::try_from_bytes(aes256gcm_siv) .map_err(|source| StorageError::DataCorruption(source.to_string()))?; diff --git a/common/gateway-storage/src/shared_keys.rs b/common/gateway-storage/src/shared_keys.rs index ef7fac2eef..9d17535fb2 100644 --- a/common/gateway-storage/src/shared_keys.rs +++ b/common/gateway-storage/src/shared_keys.rs @@ -48,7 +48,7 @@ impl SharedKeysManager { // 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_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key) VALUES (?, ?, ?, ?); UPDATE shared_keys SET From 2c2748832cc53f4104073f21ef72243f50fc440d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 16 Sep 2024 17:45:42 +0100 Subject: [PATCH 09/17] cargo fmt --- common/gateway-storage/src/models.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/gateway-storage/src/models.rs b/common/gateway-storage/src/models.rs index 49fbc05761..fac26852da 100644 --- a/common/gateway-storage/src/models.rs +++ b/common/gateway-storage/src/models.rs @@ -33,7 +33,9 @@ impl TryFrom for SharedGatewayKey { &value.derived_aes256_gcm_siv_key, &value.derived_aes128_ctr_blake3_hmac_keys_bs58, ) { - (None, None) => Err(StorageError::MissingSharedKey { id: value.client_id }), + (None, None) => Err(StorageError::MissingSharedKey { + id: value.client_id, + }), (Some(aes256gcm_siv), _) => { let current_key = SharedSymmetricKey::try_from_bytes(aes256gcm_siv) .map_err(|source| StorageError::DataCorruption(source.to_string()))?; From 8cf49770215b4faeee752d6adfdb890bfaf541c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 16 Sep 2024 17:51:00 +0100 Subject: [PATCH 10/17] assert new gateway keys zeroize on drop --- common/client-core/src/client/key_manager/mod.rs | 6 +++++- .../src/registration/handshake/shared_key/mod.rs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/common/client-core/src/client/key_manager/mod.rs b/common/client-core/src/client/key_manager/mod.rs index 01eaa88fc3..6054f571f7 100644 --- a/common/client-core/src/client/key_manager/mod.rs +++ b/common/client-core/src/client/key_manager/mod.rs @@ -3,7 +3,9 @@ use crate::client::key_manager::persistence::KeyStore; use nym_crypto::asymmetric::{encryption, identity}; -use nym_gateway_requests::registration::handshake::LegacySharedKeys; +use nym_gateway_requests::registration::handshake::{ + LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey, +}; use nym_sphinx::acknowledgements::AckKey; use rand::{CryptoRng, RngCore}; use std::sync::Arc; @@ -85,4 +87,6 @@ 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::(); } diff --git a/common/gateway-requests/src/registration/handshake/shared_key/mod.rs b/common/gateway-requests/src/registration/handshake/shared_key/mod.rs index 4fee47b6cc..7049abe095 100644 --- a/common/gateway-requests/src/registration/handshake/shared_key/mod.rs +++ b/common/gateway-requests/src/registration/handshake/shared_key/mod.rs @@ -18,7 +18,7 @@ pub mod legacy; pub type SharedKeySize = ::KeySize; -#[derive(Debug, PartialEq, Zeroize)] +#[derive(Debug, PartialEq, Zeroize, ZeroizeOnDrop)] pub enum SharedGatewayKey { Current(SharedSymmetricKey), Legacy(LegacySharedKeys), From cc32eb3904c57441e86c53931d2656d7ca8c27ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 17 Sep 2024 09:14:57 +0100 Subject: [PATCH 11/17] fixed wasm build --- .../client-core/gateways-storage/src/types.rs | 5 ----- common/client-libs/gateway-client/src/lib.rs | 2 +- .../src/registration/handshake/messages.rs | 1 + .../src/registration/handshake/state.rs | 12 +++++++---- common/wasm/client-core/src/storage/types.rs | 21 ++++++++++++++----- wasm/node-tester/src/tester.rs | 2 +- 6 files changed, 27 insertions(+), 16 deletions(-) diff --git a/common/client-core/gateways-storage/src/types.rs b/common/client-core/gateways-storage/src/types.rs index 1b6cb35265..76cb2ae5ff 100644 --- a/common/client-core/gateways-storage/src/types.rs +++ b/common/client-core/gateways-storage/src/types.rs @@ -251,11 +251,6 @@ impl TryFrom for RemoteGatewayDetails { impl<'a> From<&'a RemoteGatewayDetails> for RawRemoteGatewayDetails { fn from(value: &'a RemoteGatewayDetails) -> Self { - /* - pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option, - pub derived_aes256_gcm_siv_key: Option>, - */ - 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())), diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index 57ac4f2714..2d58349409 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -8,7 +8,7 @@ use tungstenite::{protocol::Message, Error as WsError}; pub use client::{config::GatewayClientConfig, GatewayClient, GatewayConfig}; pub use nym_gateway_requests::registration::handshake::LegacySharedKeys; -use nym_gateway_requests::registration::handshake::SharedGatewayKey; +pub use nym_gateway_requests::registration::handshake::SharedGatewayKey; pub use packet_router::{ AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender, PacketRouter, diff --git a/common/gateway-requests/src/registration/handshake/messages.rs b/common/gateway-requests/src/registration/handshake/messages.rs index d2f08708c1..80c8c961da 100644 --- a/common/gateway-requests/src/registration/handshake/messages.rs +++ b/common/gateway-requests/src/registration/handshake/messages.rs @@ -31,6 +31,7 @@ pub struct MaterialExchange { } impl MaterialExchange { + #[cfg(not(target_arch = "wasm32"))] pub fn attach_ephemeral_dh(self, ephemeral_dh: x25519::PublicKey) -> GatewayMaterialExchange { GatewayMaterialExchange { ephemeral_dh, diff --git a/common/gateway-requests/src/registration/handshake/state.rs b/common/gateway-requests/src/registration/handshake/state.rs index c1b5e1056a..05d86a8ed2 100644 --- a/common/gateway-requests/src/registration/handshake/state.rs +++ b/common/gateway-requests/src/registration/handshake/state.rs @@ -3,7 +3,7 @@ use crate::registration::handshake::error::HandshakeError; use crate::registration::handshake::messages::{ - Finalization, HandshakeMessage, Initialisation, MaterialExchange, + HandshakeMessage, Initialisation, MaterialExchange, }; use crate::registration::handshake::shared_key::SharedKeySize; use crate::registration::handshake::{LegacySharedKeySize, LegacySharedKeys, SharedSymmetricKey}; @@ -101,6 +101,7 @@ impl<'a, S> State<'a, S> { self } + #[cfg(not(target_arch = "wasm32"))] pub(crate) fn set_aes256_gcm_siv_key_derivation(&mut self, derive_aes256_gcm_siv_key: bool) { self.derive_aes256_gcm_siv_key = derive_aes256_gcm_siv_key; } @@ -121,8 +122,11 @@ impl<'a, S> State<'a, S> { } } - pub(crate) fn finalization_message(&self) -> Finalization { - Finalization { success: true } + #[cfg(not(target_arch = "wasm32"))] + pub(crate) fn finalization_message( + &self, + ) -> crate::registration::handshake::messages::Finalization { + crate::registration::handshake::messages::Finalization { success: true } } pub(crate) fn derive_shared_key(&mut self, remote_ephemeral_key: &encryption::PublicKey) { @@ -292,7 +296,7 @@ impl<'a, S> State<'a, S> { } #[cfg(target_arch = "wasm32")] - async fn _receive_handshake_message(&mut self) -> Result, HandshakeError> + async fn _receive_handshake_message_bytes(&mut self) -> Result, HandshakeError> where S: Stream + Unpin, { diff --git a/common/wasm/client-core/src/storage/types.rs b/common/wasm/client-core/src/storage/types.rs index 0973c837e8..c59d729589 100644 --- a/common/wasm/client-core/src/storage/types.rs +++ b/common/wasm/client-core/src/storage/types.rs @@ -4,7 +4,9 @@ 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; // a more nested struct since we only have a single gateway type in wasm (no 'custom') @@ -14,7 +16,10 @@ pub struct WasmRawRegisteredGateway { pub registration_timestamp: OffsetDateTime, - pub derived_aes128_ctr_blake3_hmac_keys_bs58: String, + pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option, + + #[serde(default)] + pub derived_aes256_gcm_siv_key: Option>, pub gateway_owner_address: Option, @@ -30,6 +35,7 @@ impl TryFrom for GatewayRegistration { 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, }; @@ -48,17 +54,22 @@ 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), + }; + WasmRawRegisteredGateway { gateway_id_bs58: remote_details.gateway_id.to_string(), registration_timestamp: value.registration_timestamp, - derived_aes128_ctr_blake3_hmac_keys_bs58: remote_details - .derived_aes128_ctr_blake3_hmac_keys - .to_base58_string(), + 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_listener.to_string(), } } } diff --git a/wasm/node-tester/src/tester.rs b/wasm/node-tester/src/tester.rs index b718e9c082..ef35acf759 100644 --- a/wasm/node-tester/src/tester.rs +++ b/wasm/node-tester/src/tester.rs @@ -205,7 +205,7 @@ impl NymNodeTesterBuilder { GatewayClientConfig::new_default().with_disabled_credentials_mode(true), cfg, managed_keys.identity_keypair(), - Some(gateway_info.derived_aes128_ctr_blake3_hmac_keys), + Some(gateway_info.shared_key), packet_router, self.bandwidth_controller.take(), gateway_task, From 9d8369a5b2684915d2819f1425cf2ae9f8406a2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 18 Sep 2024 10:27:02 +0100 Subject: [PATCH 12/17] generate pseudorandom salt for deriving aes256gcm-siv key --- .../src/registration/handshake/client.rs | 16 ++++-- .../src/registration/handshake/gateway.rs | 9 ++-- .../src/registration/handshake/messages.rs | 29 ++++++----- .../src/registration/handshake/mod.rs | 13 +++-- .../src/registration/handshake/state.rs | 49 +++++++++++++++---- 5 files changed, 81 insertions(+), 35 deletions(-) diff --git a/common/gateway-requests/src/registration/handshake/client.rs b/common/gateway-requests/src/registration/handshake/client.rs index aebda07551..5bdb239a66 100644 --- a/common/gateway-requests/src/registration/handshake/client.rs +++ b/common/gateway-requests/src/registration/handshake/client.rs @@ -6,16 +6,21 @@ use crate::registration::handshake::state::State; use crate::registration::handshake::SharedGatewayKey; use crate::registration::handshake::{error::HandshakeError, WsItem}; use futures::{Sink, Stream}; +use rand::{CryptoRng, RngCore}; use tungstenite::Message as WsMessage; -impl<'a, S> State<'a, S> { +impl<'a, S, R> State<'a, S, R> { async fn client_handshake_inner(&mut self) -> Result<(), HandshakeError> where S: Stream + Sink + Unpin, + R: CryptoRng + RngCore, { - // 1. send ed25519 pubkey alongside ephemeral x25519 pubkey and a flag to indicate non-legacy client - // LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_NON_LEGACY - let init_message = self.init_message(); + // 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(); + + // 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()); self.send_handshake_data(init_message).await?; // 2. wait for response with remote x25519 pubkey as well as encrypted signature @@ -26,7 +31,7 @@ impl<'a, S> State<'a, S> { // 3. derive shared keys locally // hkdf::::(g^xy) - self.derive_shared_key(&mid_res.ephemeral_dh); + self.derive_shared_key(&mid_res.ephemeral_dh, maybe_hkdf_salt.as_deref()); // 4. verify the received signature using the locally derived keys self.verify_remote_key_material(&mid_res.materials, &mid_res.ephemeral_dh)?; @@ -47,6 +52,7 @@ impl<'a, S> State<'a, S> { ) -> Result where S: Stream + Sink + Unpin, + R: CryptoRng + RngCore, { let handshake_res = self.client_handshake_inner().await; self.check_for_handshake_processing_error(handshake_res) diff --git a/common/gateway-requests/src/registration/handshake/gateway.rs b/common/gateway-requests/src/registration/handshake/gateway.rs index 877563ed53..fc439b53c0 100644 --- a/common/gateway-requests/src/registration/handshake/gateway.rs +++ b/common/gateway-requests/src/registration/handshake/gateway.rs @@ -10,7 +10,7 @@ use crate::registration::handshake::{error::HandshakeError, WsItem}; use futures::{Sink, Stream}; use tungstenite::Message as WsMessage; -impl<'a, S> State<'a, S> { +impl<'a, S, R> State<'a, S, R> { async fn gateway_handshake_inner( &mut self, raw_init_message: Vec, @@ -22,11 +22,14 @@ impl<'a, S> State<'a, S> { // LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_NON_LEGACY let init_message = Initialisation::try_from_bytes(&raw_init_message)?; self.update_remote_identity(init_message.identity); - self.set_aes256_gcm_siv_key_derivation(init_message.derive_aes256_gcm_siv_key); + self.set_aes256_gcm_siv_key_derivation(!init_message.is_legacy()); // 2. derive shared keys locally // hkdf::::(g^xy) - self.derive_shared_key(&init_message.ephemeral_dh); + self.derive_shared_key( + &init_message.ephemeral_dh, + init_message.initiator_salt.as_deref(), + ); // 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 80c8c961da..6f64c561b0 100644 --- a/common/gateway-requests/src/registration/handshake/messages.rs +++ b/common/gateway-requests/src/registration/handshake/messages.rs @@ -2,10 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 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_sphinx::params::GatewayEncryptionAlgorithm; -use std::iter::once; // it is vital nobody changes the serialisation implementation unless you have an EXTREMELY good reason, // as otherwise you have very high chance of breaking backwards compatibility @@ -21,7 +21,13 @@ pub trait HandshakeMessage { pub struct Initialisation { pub identity: ed25519::PublicKey, pub ephemeral_dh: x25519::PublicKey, - pub derive_aes256_gcm_siv_key: bool, + pub initiator_salt: Option>, +} + +impl Initialisation { + pub fn is_legacy(&self) -> bool { + self.initiator_salt.is_none() + } } #[derive(Debug)] @@ -61,7 +67,7 @@ impl Finalization { } impl HandshakeMessage for Initialisation { - // LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_NON_LEGACY + // 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. fn into_bytes(self) -> Vec { @@ -71,8 +77,8 @@ impl HandshakeMessage for Initialisation { .into_iter() .chain(self.ephemeral_dh.to_bytes()); - if self.derive_aes256_gcm_siv_key { - bytes.chain(once(1)).collect() + if let Some(salt) = self.initiator_salt { + bytes.chain(salt.into_iter()).collect() } else { bytes.collect() } @@ -84,7 +90,7 @@ impl HandshakeMessage for Initialisation { Self: Sized, { let legacy_len = ed25519::PUBLIC_KEY_LENGTH + x25519::PUBLIC_KEY_SIZE; - let current_len = legacy_len + 1; + let current_len = legacy_len + KDF_SALT_LENGTH; if bytes.len() != legacy_len && bytes.len() != current_len { return Err(HandshakeError::MalformedRequest); } @@ -97,19 +103,16 @@ impl HandshakeMessage for Initialisation { let ephemeral_dh = x25519::PublicKey::from_bytes(&bytes[ed25519::PUBLIC_KEY_LENGTH..legacy_len]).unwrap(); - let derive_aes256_gcm_siv_key = if bytes.len() == legacy_len { - false + let initiator_salt = if bytes.len() == legacy_len { + None } else { - if bytes[legacy_len] != 1 { - return Err(HandshakeError::MalformedRequest); - } - true + Some(bytes[legacy_len..].to_vec()) }; Ok(Initialisation { identity, ephemeral_dh, - derive_aes256_gcm_siv_key, + initiator_salt, }) } } diff --git a/common/gateway-requests/src/registration/handshake/mod.rs b/common/gateway-requests/src/registration/handshake/mod.rs index 1aae69f9fc..55211d7035 100644 --- a/common/gateway-requests/src/registration/handshake/mod.rs +++ b/common/gateway-requests/src/registration/handshake/mod.rs @@ -30,6 +30,9 @@ pub use self::shared_key::{ SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey, }; +// realistically even 32bit would have sufficed, so 128 is definitely enough +pub const KDF_SALT_LENGTH: usize = 16; + // Note: the handshake is built on top of WebSocket, but in principle it shouldn't be too difficult // to remove that restriction, by just changing Sink and Stream into // AsyncWrite and AsyncRead and slightly adjusting the implementation. But right now @@ -47,8 +50,8 @@ impl<'a> Future for GatewayHandshake<'a> { } } -pub fn client_handshake<'a, S>( - rng: &mut (impl RngCore + CryptoRng), +pub fn client_handshake<'a, S, R>( + rng: &'a mut R, ws_stream: &'a mut S, identity: &'a identity::KeyPair, gateway_pubkey: identity::PublicKey, @@ -58,6 +61,7 @@ pub fn client_handshake<'a, S>( ) -> GatewayHandshake<'a> where S: Stream + Sink + Unpin + Send + 'a, + R: CryptoRng + RngCore + Send, { let state = State::new( rng, @@ -76,8 +80,8 @@ where } #[cfg(not(target_arch = "wasm32"))] -pub fn gateway_handshake<'a, S>( - rng: &mut (impl RngCore + CryptoRng), +pub fn gateway_handshake<'a, S, R>( + rng: &'a mut R, ws_stream: &'a mut S, identity: &'a identity::KeyPair, received_init_payload: Vec, @@ -85,6 +89,7 @@ pub fn gateway_handshake<'a, S>( ) -> GatewayHandshake<'a> where S: Stream + Sink + Unpin + Send + 'a, + R: CryptoRng + RngCore + Send, { let state = State::new(rng, ws_stream, identity, None, shutdown); GatewayHandshake { diff --git a/common/gateway-requests/src/registration/handshake/state.rs b/common/gateway-requests/src/registration/handshake/state.rs index 05d86a8ed2..578b78ab1a 100644 --- a/common/gateway-requests/src/registration/handshake/state.rs +++ b/common/gateway-requests/src/registration/handshake/state.rs @@ -6,7 +6,9 @@ use crate::registration::handshake::messages::{ HandshakeMessage, Initialisation, MaterialExchange, }; use crate::registration::handshake::shared_key::SharedKeySize; -use crate::registration::handshake::{LegacySharedKeySize, LegacySharedKeys, SharedSymmetricKey}; +use crate::registration::handshake::{ + LegacySharedKeySize, LegacySharedKeys, SharedSymmetricKey, KDF_SALT_LENGTH, +}; use crate::registration::handshake::{SharedGatewayKey, WsItem}; use crate::{ types, AES_GCM_SIV_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, @@ -38,10 +40,13 @@ use tokio::time::timeout; use wasmtimer::tokio::timeout; /// Handshake state. -pub(crate) struct State<'a, S> { +pub(crate) struct State<'a, S, R> { /// The underlying WebSocket stream. ws_stream: &'a mut S, + /// Pseudorandom number generator used during the exchange + rng: &'a mut R, + /// Identity of the local "node" (client or gateway) which is used /// during the handshake. identity: &'a ed25519::KeyPair, @@ -68,17 +73,21 @@ pub(crate) struct State<'a, S> { shutdown: TaskClient, } -impl<'a, S> State<'a, S> { +impl<'a, S, R> State<'a, S, R> { pub(crate) fn new( - rng: &mut (impl RngCore + CryptoRng), + rng: &'a mut R, ws_stream: &'a mut S, identity: &'a identity::KeyPair, remote_pubkey: Option, #[cfg(not(target_arch = "wasm32"))] shutdown: TaskClient, - ) -> Self { + ) -> Self + where + R: CryptoRng + RngCore, + { let ephemeral_keypair = encryption::KeyPair::new(rng); State { ws_stream, + rng, ephemeral_keypair, identity, remote_pubkey, @@ -111,14 +120,27 @@ impl<'a, S> State<'a, S> { self.ephemeral_keypair.public_key() } - // LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_NON_LEGACY + pub(crate) fn maybe_generate_initiator_salt(&mut self) -> Option> + where + R: CryptoRng + RngCore, + { + if self.derive_aes256_gcm_siv_key { + let mut salt = vec![0u8; KDF_SALT_LENGTH]; + self.rng.fill_bytes(&mut salt); + Some(salt) + } else { + None + } + } + + // 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) -> Initialisation { + pub(crate) fn init_message(&self, initiator_salt: Option>) -> Initialisation { Initialisation { identity: *self.identity.public_key(), ephemeral_dh: *self.ephemeral_keypair.public_key(), - derive_aes256_gcm_siv_key: self.derive_aes256_gcm_siv_key, + initiator_salt, } } @@ -129,7 +151,11 @@ impl<'a, S> State<'a, S> { crate::registration::handshake::messages::Finalization { success: true } } - pub(crate) fn derive_shared_key(&mut self, remote_ephemeral_key: &encryption::PublicKey) { + pub(crate) fn derive_shared_key( + &mut self, + remote_ephemeral_key: &encryption::PublicKey, + initiator_salt: Option<&[u8]>, + ) { let dh_result = self .ephemeral_keypair .private_key() @@ -143,7 +169,10 @@ impl<'a, S> State<'a, S> { // there is no reason for this to fail as our okm is expected to be only 16 bytes let okm = hkdf::extract_then_expand::( - None, &dh_result, None, key_size, + initiator_salt, + &dh_result, + None, + key_size, ) .expect("somehow too long okm was provided"); From 9efeef881a6c81c8c9230eb7a2e339186dae4084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 18 Sep 2024 15:15:44 +0100 Subject: [PATCH 13/17] split types.rs + added additional helpers --- common/gateway-requests/Cargo.toml | 2 +- .../src/authentication/encrypted_address.rs | 2 +- common/gateway-requests/src/lib.rs | 7 + .../src/registration/handshake/error.rs | 2 +- .../src/registration/handshake/mod.rs | 9 +- .../src/registration/handshake/state.rs | 11 +- .../src/shared_key/helpers.rs | 98 +++ .../handshake => }/shared_key/legacy.rs | 31 +- .../handshake => }/shared_key/mod.rs | 33 +- common/gateway-requests/src/types.rs | 680 ------------------ .../src/types/binary_request.rs | 77 ++ .../src/types/binary_response.rs | 71 ++ common/gateway-requests/src/types/error.rs | 98 +++ common/gateway-requests/src/types/helpers.rs | 133 ++++ common/gateway-requests/src/types/mod.rs | 18 + .../types/registration_handshake_wrapper.rs | 103 +++ .../src/types/text_request.rs | 197 +++++ .../src/types/text_response.rs | 128 ++++ 18 files changed, 989 insertions(+), 711 deletions(-) create mode 100644 common/gateway-requests/src/shared_key/helpers.rs rename common/gateway-requests/src/{registration/handshake => }/shared_key/legacy.rs (85%) rename common/gateway-requests/src/{registration/handshake => }/shared_key/mod.rs (91%) delete mode 100644 common/gateway-requests/src/types.rs create mode 100644 common/gateway-requests/src/types/binary_request.rs create mode 100644 common/gateway-requests/src/types/binary_response.rs create mode 100644 common/gateway-requests/src/types/error.rs create mode 100644 common/gateway-requests/src/types/helpers.rs create mode 100644 common/gateway-requests/src/types/mod.rs create mode 100644 common/gateway-requests/src/types/registration_handshake_wrapper.rs create mode 100644 common/gateway-requests/src/types/text_request.rs create mode 100644 common/gateway-requests/src/types/text_response.rs diff --git a/common/gateway-requests/Cargo.toml b/common/gateway-requests/Cargo.toml index 2b8a106794..b461c428c1 100644 --- a/common/gateway-requests/Cargo.toml +++ b/common/gateway-requests/Cargo.toml @@ -22,7 +22,7 @@ thiserror = { workspace = true } tracing = { workspace = true, features = ["log"] } zeroize = { workspace = true } -nym-crypto = { path = "../crypto", features = ["aead"] } +nym-crypto = { path = "../crypto", features = ["aead", "hashing"] } nym-pemstore = { path = "../pemstore" } nym-sphinx = { path = "../nymsphinx" } nym-task = { path = "../task" } diff --git a/common/gateway-requests/src/authentication/encrypted_address.rs b/common/gateway-requests/src/authentication/encrypted_address.rs index f4a2f3e3a8..8b81074454 100644 --- a/common/gateway-requests/src/authentication/encrypted_address.rs +++ b/common/gateway-requests/src/authentication/encrypted_address.rs @@ -1,7 +1,7 @@ // Copyright 2020-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::registration::handshake::{SharedGatewayKey, SharedKeyUsageError}; +use crate::shared_key::{SharedGatewayKey, SharedKeyUsageError}; use nym_sphinx::DestinationAddressBytes; use thiserror::Error; diff --git a/common/gateway-requests/src/lib.rs b/common/gateway-requests/src/lib.rs index 4b83be8c10..b7af197cc5 100644 --- a/common/gateway-requests/src/lib.rs +++ b/common/gateway-requests/src/lib.rs @@ -10,8 +10,15 @@ 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 const CURRENT_PROTOCOL_VERSION: u8 = AES_GCM_SIV_PROTOCOL_VERSION; /// Defines the current version of the communication protocol between gateway and clients. diff --git a/common/gateway-requests/src/registration/handshake/error.rs b/common/gateway-requests/src/registration/handshake/error.rs index f78ec07af5..8cc9cf1c0d 100644 --- a/common/gateway-requests/src/registration/handshake/error.rs +++ b/common/gateway-requests/src/registration/handshake/error.rs @@ -1,7 +1,7 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::registration::handshake::shared_key::SharedKeyUsageError; +use crate::shared_key::SharedKeyUsageError; use thiserror::Error; #[derive(Debug, Error)] diff --git a/common/gateway-requests/src/registration/handshake/mod.rs b/common/gateway-requests/src/registration/handshake/mod.rs index 55211d7035..e5dc1dc59f 100644 --- a/common/gateway-requests/src/registration/handshake/mod.rs +++ b/common/gateway-requests/src/registration/handshake/mod.rs @@ -1,8 +1,9 @@ -// Copyright 2020 - Nym Technologies SA +// Copyright 2020-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use self::error::HandshakeError; use crate::registration::handshake::state::State; +use crate::SharedGatewayKey; use futures::future::BoxFuture; use futures::{Sink, Stream}; use nym_crypto::asymmetric::identity; @@ -22,14 +23,8 @@ pub mod error; #[cfg(not(target_arch = "wasm32"))] mod gateway; mod messages; -mod shared_key; mod state; -pub use self::shared_key::legacy::{LegacySharedKeySize, LegacySharedKeys}; -pub use self::shared_key::{ - SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey, -}; - // realistically even 32bit would have sufficed, so 128 is definitely enough pub const KDF_SALT_LENGTH: usize = 16; diff --git a/common/gateway-requests/src/registration/handshake/state.rs b/common/gateway-requests/src/registration/handshake/state.rs index 578b78ab1a..ab7ae4d97f 100644 --- a/common/gateway-requests/src/registration/handshake/state.rs +++ b/common/gateway-requests/src/registration/handshake/state.rs @@ -5,14 +5,11 @@ use crate::registration::handshake::error::HandshakeError; use crate::registration::handshake::messages::{ HandshakeMessage, Initialisation, MaterialExchange, }; -use crate::registration::handshake::shared_key::SharedKeySize; -use crate::registration::handshake::{ - LegacySharedKeySize, LegacySharedKeys, SharedSymmetricKey, KDF_SALT_LENGTH, -}; -use crate::registration::handshake::{SharedGatewayKey, WsItem}; +use crate::registration::handshake::{SharedGatewayKey, WsItem, KDF_SALT_LENGTH}; +use crate::shared_key::SharedKeySize; use crate::{ - types, AES_GCM_SIV_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, - INITIAL_PROTOCOL_VERSION, + types, LegacySharedKeySize, LegacySharedKeys, SharedSymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION, + CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, }; use futures::{Sink, SinkExt, Stream, StreamExt}; use nym_crypto::asymmetric::{ed25519, x25519}; diff --git a/common/gateway-requests/src/shared_key/helpers.rs b/common/gateway-requests/src/shared_key/helpers.rs new file mode 100644 index 0000000000..ef034f9752 --- /dev/null +++ b/common/gateway-requests/src/shared_key/helpers.rs @@ -0,0 +1,98 @@ +// 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/registration/handshake/shared_key/legacy.rs b/common/gateway-requests/src/shared_key/legacy.rs similarity index 85% rename from common/gateway-requests/src/registration/handshake/shared_key/legacy.rs rename to common/gateway-requests/src/shared_key/legacy.rs index 3d6bb905f0..20820808f2 100644 --- a/common/gateway-requests/src/registration/handshake/shared_key/legacy.rs +++ b/common/gateway-requests/src/shared_key/legacy.rs @@ -1,18 +1,24 @@ // Copyright 2020-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::registration::handshake::shared_key::{SharedKeyConversionError, SharedKeyUsageError}; +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, LegacyGatewayEncryptionAlgorithm}; +use nym_sphinx::params::{ + GatewayIntegrityHmacAlgorithm, GatewaySharedKeyHkdfAlgorithm, LegacyGatewayEncryptionAlgorithm, +}; +use rand::{thread_rng, RngCore}; use serde::{Deserialize, Serialize}; -use zeroize::{Zeroize, ZeroizeOnDrop}; +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; // shared key is as long as the encryption key and the MAC key combined. pub type LegacySharedKeySize = Sum; @@ -31,6 +37,25 @@ pub struct LegacySharedKeys { } 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()); + let okm = hkdf::extract_then_expand::( + Some(&salt), + &legacy_bytes, + None, + SharedKeySize::to_usize(), + ) + .expect("somehow too long okm was provided"); + + let key = SharedSymmetricKey::try_from_bytes(&okm) + .expect("okm was expanded to incorrect length!"); + (key, salt) + } + pub fn try_from_bytes(bytes: &[u8]) -> Result { if bytes.len() != LegacySharedKeySize::to_usize() { return Err(SharedKeyConversionError::InvalidSharedKeysSize { diff --git a/common/gateway-requests/src/registration/handshake/shared_key/mod.rs b/common/gateway-requests/src/shared_key/mod.rs similarity index 91% rename from common/gateway-requests/src/registration/handshake/shared_key/mod.rs rename to common/gateway-requests/src/shared_key/mod.rs index 7049abe095..009f43eca2 100644 --- a/common/gateway-requests/src/registration/handshake/shared_key/mod.rs +++ b/common/gateway-requests/src/shared_key/mod.rs @@ -1,7 +1,9 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::registration::handshake::LegacySharedKeys; +use crate::LegacySharedKeys; +use nym_crypto::blake3; +use nym_crypto::crypto_hash::compute_digest; use nym_crypto::generic_array::{typenum::Unsigned, GenericArray}; use nym_crypto::symmetric::aead::{ self, nonce_size, random_nonce, AeadError, AeadKey, KeySizeUser, Nonce, @@ -14,6 +16,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; +pub mod helpers; pub mod legacy; pub type SharedKeySize = ::KeySize; @@ -108,7 +111,7 @@ pub enum SharedKeyUsageError { } impl SharedGatewayKey { - fn aead_nonce( + fn validate_aead_nonce( raw: Option<&[u8]>, ) -> Result, SharedKeyUsageError> { let Some(raw) = raw else { @@ -120,7 +123,7 @@ impl SharedGatewayKey { Ok(Nonce::::clone_from_slice(raw)) } - fn cipher_iv( + fn validate_cipher_iv( raw: Option<&[u8]>, ) -> Result>, SharedKeyUsageError> { let Some(raw) = raw else { return Ok(None) }; @@ -143,11 +146,11 @@ impl SharedGatewayKey { ) -> Result, SharedKeyUsageError> { match self { SharedGatewayKey::Current(aes_gcm_siv) => { - let nonce = Self::aead_nonce(raw_nonce)?; + let nonce = Self::validate_aead_nonce(raw_nonce)?; aes_gcm_siv.encrypt(plaintext, &nonce) } SharedGatewayKey::Legacy(aes_ctr) => { - let iv = Self::cipher_iv(raw_nonce)?; + let iv = Self::validate_cipher_iv(raw_nonce)?; Ok(aes_ctr.encrypt_and_tag(plaintext, iv)) } } @@ -161,11 +164,11 @@ impl SharedGatewayKey { ) -> Result, SharedKeyUsageError> { match self { SharedGatewayKey::Current(aes_gcm_siv) => { - let nonce = Self::aead_nonce(raw_nonce)?; + let nonce = Self::validate_aead_nonce(raw_nonce)?; aes_gcm_siv.decrypt(ciphertext, &nonce) } SharedGatewayKey::Legacy(aes_ctr) => { - let iv = Self::cipher_iv(raw_nonce)?; + let iv = Self::validate_cipher_iv(raw_nonce)?; aes_ctr.decrypt_tagged(ciphertext, iv) } } @@ -180,11 +183,11 @@ impl SharedGatewayKey { ) -> Result, SharedKeyUsageError> { match self { SharedGatewayKey::Current(aes_gcm_siv) => { - let nonce = Self::aead_nonce(raw_nonce)?; + let nonce = Self::validate_aead_nonce(raw_nonce)?; aes_gcm_siv.encrypt(plaintext, &nonce) } SharedGatewayKey::Legacy(aes_ctr) => { - let iv = Self::cipher_iv(raw_nonce)?; + let iv = Self::validate_cipher_iv(raw_nonce)?; Ok(aes_ctr.encrypt_without_tagging(plaintext, iv)) } } @@ -199,11 +202,11 @@ impl SharedGatewayKey { ) -> Result, SharedKeyUsageError> { match self { SharedGatewayKey::Current(aes_gcm_siv) => { - let nonce = Self::aead_nonce(raw_nonce)?; + let nonce = Self::validate_aead_nonce(raw_nonce)?; aes_gcm_siv.decrypt(ciphertext, &nonce) } SharedGatewayKey::Legacy(aes_ctr) => { - let iv = Self::cipher_iv(raw_nonce)?; + let iv = Self::validate_cipher_iv(raw_nonce)?; aes_ctr.decrypt_without_tag(ciphertext, iv) } } @@ -237,6 +240,14 @@ impl SharedSymmetricKey { Ok(SharedSymmetricKey(GenericArray::clone_from_slice(bytes))) } + pub fn digest(&self) -> Vec { + compute_digest::(self.as_bytes()).to_vec() + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_slice() + } + pub fn to_bytes(&self) -> Vec { self.0.iter().copied().collect() } diff --git a/common/gateway-requests/src/types.rs b/common/gateway-requests/src/types.rs deleted file mode 100644 index b223502b39..0000000000 --- a/common/gateway-requests/src/types.rs +++ /dev/null @@ -1,680 +0,0 @@ -// Copyright 2020-2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::models::CredentialSpendingRequest; -use crate::registration::handshake::{SharedGatewayKey, SharedKeyUsageError}; -use crate::{ - AES_GCM_SIV_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, -}; -use nym_credentials::ecash::bandwidth::CredentialSpendingData; -use nym_credentials_interface::CompactEcashError; -use nym_sphinx::addressing::nodes::NymNodeRoutingAddressError; -use nym_sphinx::forwarding::packet::{MixPacket, MixPacketFormattingError}; -use nym_sphinx::params::packet_sizes::PacketSize; -use nym_sphinx::DestinationAddressBytes; -use serde::{Deserialize, Serialize}; -use std::iter::once; -use std::str::FromStr; -use std::string::FromUtf8Error; -use strum::FromRepr; -use thiserror::Error; -use tracing::log::error; -use tungstenite::protocol::Message; - -#[derive(Serialize, Deserialize, Debug)] -#[serde(tag = "type", rename_all = "camelCase")] -pub enum RegistrationHandshake { - HandshakePayload { - #[serde(default)] - protocol_version: Option, - data: Vec, - }, - HandshakeError { - message: String, - }, -} - -impl RegistrationHandshake { - pub fn new_payload(data: Vec, protocol_version: u8) -> Self { - RegistrationHandshake::HandshakePayload { - protocol_version: Some(protocol_version), - data, - } - } - - pub fn new_error>(message: S) -> Self { - RegistrationHandshake::HandshakeError { - message: message.into(), - } - } -} - -impl FromStr for RegistrationHandshake { - type Err = serde_json::Error; - - fn from_str(s: &str) -> Result { - serde_json::from_str(s) - } -} - -impl TryFrom for RegistrationHandshake { - type Error = serde_json::Error; - - fn try_from(msg: String) -> Result { - msg.parse() - } -} - -impl TryInto for RegistrationHandshake { - type Error = serde_json::Error; - - fn try_into(self) -> Result { - serde_json::to_string(&self) - } -} - -// specific errors (that should not be nested!!) for clients to match on -#[derive(Debug, Copy, Clone, Error, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum SimpleGatewayRequestsError { - #[error("insufficient bandwidth available to process the request. required: {required}B, available: {available}B")] - OutOfBandwidth { required: i64, available: i64 }, - - #[error("the provided ticket has already been spent before at this gateway")] - TicketReplay, -} - -impl SimpleGatewayRequestsError { - pub fn is_ticket_replay(&self) -> bool { - matches!(self, SimpleGatewayRequestsError::TicketReplay) - } -} - -#[derive(Debug, Error)] -pub enum GatewayRequestsError { - #[error(transparent)] - KeyUsageFailure(#[from] SharedKeyUsageError), - - #[error("received request with an unknown kind: {kind}")] - UnknownRequestKind { kind: u8 }, - - #[error("received response with an unknown kind: {kind}")] - UnknownResponseKind { kind: u8 }, - - #[error("the encryption flag had an unexpected value")] - InvalidEncryptionFlag, - - #[error("the request is too short")] - TooShortRequest, - - #[error("provided MAC is invalid")] - InvalidMac, - - #[error("address field was incorrectly encoded: {source}")] - IncorrectlyEncodedAddress { - #[from] - source: NymNodeRoutingAddressError, - }, - - #[error("received request had invalid size. (actual: {0}, but expected one of: {} (ACK), {} (REGULAR), {}, {}, {} (EXTENDED))", - PacketSize::AckPacket.size(), - PacketSize::RegularPacket.size(), - PacketSize::ExtendedPacket8.size(), - PacketSize::ExtendedPacket16.size(), - PacketSize::ExtendedPacket32.size()) - ] - RequestOfInvalidSize(usize), - - #[error("received sphinx packet was malformed")] - MalformedSphinxPacket, - - #[error("failed to serialise created sphinx packet: {0}")] - SphinxSerialisationFailure(#[from] MixPacketFormattingError), - - #[error("the received encrypted data was malformed")] - MalformedEncryption, - - #[error("provided packet mode is invalid")] - InvalidPacketMode, - - #[error("failed to deserialize provided credential: {0}")] - EcashCredentialDeserializationFailure(#[from] CompactEcashError), - - #[error("failed to deserialize provided credential: EOF")] - CredentialDeserializationFailureEOF, - - #[error("failed to deserialize provided credential: malformed string: {0}")] - CredentialDeserializationFailureMalformedString(#[from] FromUtf8Error), - - #[error("the provided [v1] credential has invalid number of parameters - {0}")] - InvalidNumberOfEmbededParameters(u32), - - // variant to catch legacy errors - #[error("{0}")] - Other(String), -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(tag = "type", rename_all = "camelCase")] -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, - }, - #[serde(alias = "handshakePayload")] - RegisterHandshakeInitRequest { - #[serde(default)] - protocol_version: Option, - data: Vec, - }, - BandwidthCredential { - enc_credential: Vec, - iv: Vec, - }, - BandwidthCredentialV2 { - enc_credential: Vec, - iv: Vec, - }, - EcashCredential { - enc_credential: Vec, - iv: Vec, - }, - ClaimFreeTestnetBandwidth, - SupportedProtocol {}, -} - -impl ClientControlRequest { - pub fn new_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 - 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 name(&self) -> String { - match self { - ClientControlRequest::Authenticate { .. } => "Authenticate".to_string(), - ClientControlRequest::RegisterHandshakeInitRequest { .. } => { - "RegisterHandshakeInitRequest".to_string() - } - ClientControlRequest::BandwidthCredential { .. } => "BandwidthCredential".to_string(), - ClientControlRequest::BandwidthCredentialV2 { .. } => { - "BandwidthCredentialV2".to_string() - } - ClientControlRequest::EcashCredential { .. } => "EcashCredential".to_string(), - ClientControlRequest::ClaimFreeTestnetBandwidth => { - "ClaimFreeTestnetBandwidth".to_string() - } - ClientControlRequest::SupportedProtocol { .. } => "SupportedProtocol".to_string(), - } - } - - pub fn new_enc_ecash_credential( - credential: CredentialSpendingData, - shared_key: &SharedGatewayKey, - ) -> 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))?; - - Ok(ClientControlRequest::EcashCredential { - enc_credential, - iv: nonce, - }) - } - - pub fn try_from_enc_ecash_credential( - enc_credential: Vec, - shared_key: &SharedGatewayKey, - iv: Vec, - ) -> Result { - let credential_bytes = shared_key.decrypt(&enc_credential, Some(&iv))?; - CredentialSpendingRequest::try_from_bytes(credential_bytes.as_slice()) - .map_err(|_| GatewayRequestsError::MalformedEncryption) - } -} - -impl From for Message { - fn from(req: ClientControlRequest) -> Self { - // it should be safe to call `unwrap` here as the message is generated by the server - // so if it fails (and consequently panics) it's a bug that should be resolved - let str_req = serde_json::to_string(&req).unwrap(); - Message::Text(str_req) - } -} - -impl TryFrom for ClientControlRequest { - type Error = serde_json::Error; - - fn try_from(msg: String) -> Result { - msg.parse() - } -} - -impl FromStr for ClientControlRequest { - type Err = serde_json::Error; - - fn from_str(s: &str) -> Result { - serde_json::from_str(s) - } -} - -impl TryInto for ClientControlRequest { - type Error = serde_json::Error; - - fn try_into(self) -> Result { - serde_json::to_string(&self) - } -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(tag = "type", rename_all = "camelCase")] -pub enum ServerResponse { - Authenticate { - #[serde(default)] - protocol_version: Option, - status: bool, - bandwidth_remaining: i64, - }, - Register { - #[serde(default)] - protocol_version: Option, - status: bool, - }, - Bandwidth { - available_total: i64, - }, - Send { - remaining_bandwidth: i64, - }, - SupportedProtocol { - version: u8, - }, - // Generic error - Error { - message: String, - }, - // Specific typed errors - // so that clients could match on this variant without doing naive string matching - TypedError { - error: SimpleGatewayRequestsError, - }, -} - -impl ServerResponse { - pub fn name(&self) -> String { - match self { - ServerResponse::Authenticate { .. } => "Authenticate".to_string(), - ServerResponse::Register { .. } => "Register".to_string(), - ServerResponse::Bandwidth { .. } => "Bandwidth".to_string(), - ServerResponse::Send { .. } => "Send".to_string(), - ServerResponse::Error { .. } => "Error".to_string(), - ServerResponse::TypedError { .. } => "TypedError".to_string(), - ServerResponse::SupportedProtocol { .. } => "SupportedProtocol".to_string(), - } - } - pub fn new_error>(msg: S) -> Self { - ServerResponse::Error { - message: msg.into(), - } - } - - pub fn is_error(&self) -> bool { - matches!(self, ServerResponse::Error { .. }) - } - - pub fn implies_successful_authentication(&self) -> bool { - match self { - ServerResponse::Authenticate { status, .. } => *status, - ServerResponse::Register { status, .. } => *status, - _ => false, - } - } -} - -impl From for Message { - fn from(res: ServerResponse) -> Self { - // it should be safe to call `unwrap` here as the message is generated by the server - // so if it fails (and consequently panics) it's a bug that should be resolved - let str_res = serde_json::to_string(&res).unwrap(); - Message::Text(str_res) - } -} - -impl TryFrom for ServerResponse { - type Error = serde_json::Error; - - fn try_from(msg: String) -> Result { - serde_json::from_str(&msg) - } -} - -// each binary message consists of the following structure (for non-legacy messages) -// KIND || ENC_FLAG || MAYBE_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 -// finally, the rest of the message is the associated ciphertext or just plaintext (if message wasn't encrypted) -pub struct BinaryData<'a> { - kind: u8, - encrypted: bool, - maybe_nonce: Option<&'a [u8]>, - data: &'a [u8], -} - -impl<'a> BinaryData<'a> { - // serialises possibly encrypted data into bytes to be put on the wire - pub fn into_raw(self, legacy: bool) -> Vec { - if legacy { - return self.data.to_vec(); - } - - let i = once(self.kind).chain(once(if self.encrypted { 1 } else { 0 })); - if let Some(nonce) = self.maybe_nonce { - i.chain(nonce.iter().copied()) - .chain(self.data.iter().copied()) - .collect() - } else { - i.chain(self.data.iter().copied()).collect() - } - } - - // attempts to perform basic parsing on bytes received from the wire - pub fn from_raw( - raw: &'a [u8], - available_key: &SharedGatewayKey, - ) -> 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); - } - - let kind = raw[0]; - let encrypted = if raw[1] == 1 { - true - } else if raw[1] == 0 { - false - } else { - return Err(GatewayRequestsError::InvalidEncryptionFlag); - }; - - // if data is encrypted, there MUST be a nonce present for non-legacy keys - if encrypted && raw.len() < available_key.nonce_size() + 2 { - return Err(GatewayRequestsError::TooShortRequest); - } - - Ok(BinaryData { - kind, - encrypted, - maybe_nonce: Some(&raw[2..2 + available_key.nonce_size()]), - data: &raw[2 + available_key.nonce_size()..], - }) - } - - // attempt to encrypt plaintext of provided response/request and serialise it into wire format - pub fn make_encrypted_blob( - kind: u8, - plaintext: &[u8], - key: &SharedGatewayKey, - ) -> Result, GatewayRequestsError> { - let maybe_nonce = key.random_nonce_or_zero_iv(); - - let ciphertext = key.encrypt(plaintext, maybe_nonce.as_deref())?; - Ok(BinaryData { - kind, - encrypted: true, - maybe_nonce: maybe_nonce.as_deref(), - data: &ciphertext, - } - .into_raw(key.is_legacy())) - } - - // attempts to parse previously recovered bytes into a [`BinaryRequest`] - pub fn into_request( - self, - key: &SharedGatewayKey, - ) -> 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)? - } else { - self.data - }; - - BinaryRequest::from_plaintext(kind, plaintext) - } - - // attempts to parse previously recovered bytes into a [`BinaryResponse`] - pub fn into_response( - self, - key: &SharedGatewayKey, - ) -> 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)? - } else { - self.data - }; - - BinaryResponse::from_plaintext(kind, plaintext) - } -} - -// in legacy mode requests use zero IV without -pub enum BinaryRequest { - ForwardSphinx { packet: MixPacket }, -} - -#[repr(u8)] -#[derive(Debug, Clone, Copy, FromRepr, PartialEq)] -#[non_exhaustive] -pub enum BinaryRequestKind { - ForwardSphinx = 1, -} - -// Right now the only valid `BinaryRequest` is a request to forward a sphinx packet. -// It is encrypted using the derived shared key between client and the gateway. Thanks to -// randomness inside the sphinx packet themselves (even via the same route), the 0s IV can be used here. -// HOWEVER, NOTE: If we introduced another 'BinaryRequest', we must carefully examine if a 0s IV -// would work there. -impl BinaryRequest { - pub fn kind(&self) -> BinaryRequestKind { - match self { - BinaryRequest::ForwardSphinx { .. } => BinaryRequestKind::ForwardSphinx, - } - } - - pub fn from_plaintext( - kind: BinaryRequestKind, - plaintext: &[u8], - ) -> Result { - match kind { - BinaryRequestKind::ForwardSphinx => { - let packet = MixPacket::try_from_bytes(plaintext)?; - Ok(BinaryRequest::ForwardSphinx { packet }) - } - } - } - - pub fn try_from_encrypted_tagged_bytes( - bytes: Vec, - shared_key: &SharedGatewayKey, - ) -> Result { - BinaryData::from_raw(&bytes, shared_key)?.into_request(shared_key) - } - - pub fn into_encrypted_tagged_bytes( - self, - shared_key: &SharedGatewayKey, - ) -> Result, GatewayRequestsError> { - let kind = self.kind(); - - let plaintext = match self { - BinaryRequest::ForwardSphinx { packet } => packet.into_bytes()?, - }; - - BinaryData::make_encrypted_blob(kind as u8, &plaintext, shared_key) - } - - pub fn into_ws_message( - self, - shared_key: &SharedGatewayKey, - ) -> Result { - // all variants are currently encrypted - let blob = match self { - BinaryRequest::ForwardSphinx { .. } => self.into_encrypted_tagged_bytes(shared_key)?, - }; - - Ok(Message::Binary(blob)) - } -} - -pub enum BinaryResponse { - PushedMixMessage { message: Vec }, -} - -#[repr(u8)] -#[derive(Debug, Clone, Copy, FromRepr, PartialEq)] -#[non_exhaustive] -pub enum BinaryResponseKind { - PushedMixMessage = 1, -} - -impl BinaryResponse { - pub fn kind(&self) -> BinaryResponseKind { - match self { - BinaryResponse::PushedMixMessage { .. } => BinaryResponseKind::PushedMixMessage, - } - } - - pub fn from_plaintext( - kind: BinaryResponseKind, - plaintext: &[u8], - ) -> Result { - match kind { - BinaryResponseKind::PushedMixMessage => Ok(BinaryResponse::PushedMixMessage { - message: plaintext.to_vec(), - }), - } - } - - pub fn try_from_encrypted_tagged_bytes( - bytes: Vec, - shared_key: &SharedGatewayKey, - ) -> Result { - BinaryData::from_raw(&bytes, shared_key)?.into_response(shared_key) - } - - pub fn into_encrypted_tagged_bytes( - self, - shared_key: &SharedGatewayKey, - ) -> Result, GatewayRequestsError> { - let kind = self.kind(); - - let plaintext = match self { - BinaryResponse::PushedMixMessage { message } => message, - }; - - BinaryData::make_encrypted_blob(kind as u8, &plaintext, shared_key) - } - - pub fn into_ws_message( - self, - shared_key: &SharedGatewayKey, - ) -> Result { - // all variants are currently encrypted - let blob = match self { - BinaryResponse::PushedMixMessage { .. } => { - self.into_encrypted_tagged_bytes(shared_key)? - } - }; - - Ok(Message::Binary(blob)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - 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), - data: handshake_data.clone(), - }; - let serialized = serde_json::to_string(&handshake_payload_with_protocol).unwrap(); - let deserialized = ClientControlRequest::try_from(serialized).unwrap(); - - match deserialized { - ClientControlRequest::RegisterHandshakeInitRequest { - protocol_version, - data, - } => { - assert_eq!(protocol_version, Some(42)); - assert_eq!(data, handshake_data) - } - _ => unreachable!("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!(data, handshake_data) - } - _ => unreachable!("this branch shouldn't have been reached!"), - } - } -} diff --git a/common/gateway-requests/src/types/binary_request.rs b/common/gateway-requests/src/types/binary_request.rs new file mode 100644 index 0000000000..3b0f21233f --- /dev/null +++ b/common/gateway-requests/src/types/binary_request.rs @@ -0,0 +1,77 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::types::helpers::BinaryData; +use crate::{GatewayRequestsError, SharedGatewayKey}; +use nym_sphinx::forwarding::packet::MixPacket; +use strum::FromRepr; +use tungstenite::Message; + +// in legacy mode requests use zero IV without +pub enum BinaryRequest { + ForwardSphinx { packet: MixPacket }, +} + +#[repr(u8)] +#[derive(Debug, Clone, Copy, FromRepr, PartialEq)] +#[non_exhaustive] +pub enum BinaryRequestKind { + ForwardSphinx = 1, +} + +// Right now the only valid `BinaryRequest` is a request to forward a sphinx packet. +// It is encrypted using the derived shared key between client and the gateway. Thanks to +// randomness inside the sphinx packet themselves (even via the same route), the 0s IV can be used here. +// HOWEVER, NOTE: If we introduced another 'BinaryRequest', we must carefully examine if a 0s IV +// would work there. +impl BinaryRequest { + pub fn kind(&self) -> BinaryRequestKind { + match self { + BinaryRequest::ForwardSphinx { .. } => BinaryRequestKind::ForwardSphinx, + } + } + + pub fn from_plaintext( + kind: BinaryRequestKind, + plaintext: &[u8], + ) -> Result { + match kind { + BinaryRequestKind::ForwardSphinx => { + let packet = MixPacket::try_from_bytes(plaintext)?; + Ok(BinaryRequest::ForwardSphinx { packet }) + } + } + } + + pub fn try_from_encrypted_tagged_bytes( + bytes: Vec, + shared_key: &SharedGatewayKey, + ) -> Result { + BinaryData::from_raw(&bytes, shared_key)?.into_request(shared_key) + } + + pub fn into_encrypted_tagged_bytes( + self, + shared_key: &SharedGatewayKey, + ) -> Result, GatewayRequestsError> { + let kind = self.kind(); + + let plaintext = match self { + BinaryRequest::ForwardSphinx { packet } => packet.into_bytes()?, + }; + + BinaryData::make_encrypted_blob(kind as u8, &plaintext, shared_key) + } + + pub fn into_ws_message( + self, + shared_key: &SharedGatewayKey, + ) -> Result { + // all variants are currently encrypted + let blob = match self { + BinaryRequest::ForwardSphinx { .. } => self.into_encrypted_tagged_bytes(shared_key)?, + }; + + Ok(Message::Binary(blob)) + } +} diff --git a/common/gateway-requests/src/types/binary_response.rs b/common/gateway-requests/src/types/binary_response.rs new file mode 100644 index 0000000000..af1551f013 --- /dev/null +++ b/common/gateway-requests/src/types/binary_response.rs @@ -0,0 +1,71 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::types::helpers::BinaryData; +use crate::{GatewayRequestsError, SharedGatewayKey}; +use strum::FromRepr; +use tungstenite::Message; + +pub enum BinaryResponse { + PushedMixMessage { message: Vec }, +} + +#[repr(u8)] +#[derive(Debug, Clone, Copy, FromRepr, PartialEq)] +#[non_exhaustive] +pub enum BinaryResponseKind { + PushedMixMessage = 1, +} + +impl BinaryResponse { + pub fn kind(&self) -> BinaryResponseKind { + match self { + BinaryResponse::PushedMixMessage { .. } => BinaryResponseKind::PushedMixMessage, + } + } + + pub fn from_plaintext( + kind: BinaryResponseKind, + plaintext: &[u8], + ) -> Result { + match kind { + BinaryResponseKind::PushedMixMessage => Ok(BinaryResponse::PushedMixMessage { + message: plaintext.to_vec(), + }), + } + } + + pub fn try_from_encrypted_tagged_bytes( + bytes: Vec, + shared_key: &SharedGatewayKey, + ) -> Result { + BinaryData::from_raw(&bytes, shared_key)?.into_response(shared_key) + } + + pub fn into_encrypted_tagged_bytes( + self, + shared_key: &SharedGatewayKey, + ) -> Result, GatewayRequestsError> { + let kind = self.kind(); + + let plaintext = match self { + BinaryResponse::PushedMixMessage { message } => message, + }; + + BinaryData::make_encrypted_blob(kind as u8, &plaintext, shared_key) + } + + pub fn into_ws_message( + self, + shared_key: &SharedGatewayKey, + ) -> Result { + // all variants are currently encrypted + let blob = match self { + BinaryResponse::PushedMixMessage { .. } => { + self.into_encrypted_tagged_bytes(shared_key)? + } + }; + + Ok(Message::Binary(blob)) + } +} diff --git a/common/gateway-requests/src/types/error.rs b/common/gateway-requests/src/types/error.rs new file mode 100644 index 0000000000..2ffb8e665b --- /dev/null +++ b/common/gateway-requests/src/types/error.rs @@ -0,0 +1,98 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::SharedKeyUsageError; +use nym_credentials_interface::CompactEcashError; +use nym_sphinx::addressing::nodes::NymNodeRoutingAddressError; +use nym_sphinx::forwarding::packet::MixPacketFormattingError; +use nym_sphinx::params::packet_sizes::PacketSize; +use serde::{Deserialize, Serialize}; +use std::string::FromUtf8Error; +use thiserror::Error; + +// specific errors (that should not be nested!!) for clients to match on +#[derive(Debug, Copy, Clone, Error, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum SimpleGatewayRequestsError { + #[error("insufficient bandwidth available to process the request. required: {required}B, available: {available}B")] + OutOfBandwidth { required: i64, available: i64 }, + + #[error("the provided ticket has already been spent before at this gateway")] + TicketReplay, +} + +impl SimpleGatewayRequestsError { + pub fn is_ticket_replay(&self) -> bool { + matches!(self, SimpleGatewayRequestsError::TicketReplay) + } +} + +#[derive(Debug, Error)] +pub enum GatewayRequestsError { + #[error(transparent)] + KeyUsageFailure(#[from] SharedKeyUsageError), + + #[error("the received request is malformed: {source}")] + MalformedRequest { source: serde_json::Error }, + + #[error("the received response is malformed: {source}")] + MalformedResponse { source: serde_json::Error }, + + #[error("received request with an unknown kind: {kind}")] + UnknownRequestKind { kind: u8 }, + + #[error("received response with an unknown kind: {kind}")] + UnknownResponseKind { kind: u8 }, + + #[error("the encryption flag had an unexpected value")] + InvalidEncryptionFlag, + + #[error("the request is too short")] + TooShortRequest, + + #[error("provided MAC is invalid")] + InvalidMac, + + #[error("address field was incorrectly encoded: {source}")] + IncorrectlyEncodedAddress { + #[from] + source: NymNodeRoutingAddressError, + }, + + #[error("received request had invalid size. (actual: {0}, but expected one of: {} (ACK), {} (REGULAR), {}, {}, {} (EXTENDED))", + PacketSize::AckPacket.size(), + PacketSize::RegularPacket.size(), + PacketSize::ExtendedPacket8.size(), + PacketSize::ExtendedPacket16.size(), + PacketSize::ExtendedPacket32.size()) + ] + RequestOfInvalidSize(usize), + + #[error("received sphinx packet was malformed")] + MalformedSphinxPacket, + + #[error("failed to serialise created sphinx packet: {0}")] + SphinxSerialisationFailure(#[from] MixPacketFormattingError), + + #[error("the received encrypted data was malformed")] + MalformedEncryption, + + #[error("provided packet mode is invalid")] + InvalidPacketMode, + + #[error("failed to deserialize provided credential: {0}")] + EcashCredentialDeserializationFailure(#[from] CompactEcashError), + + #[error("failed to deserialize provided credential: EOF")] + CredentialDeserializationFailureEOF, + + #[error("failed to deserialize provided credential: malformed string: {0}")] + CredentialDeserializationFailureMalformedString(#[from] FromUtf8Error), + + #[error("the provided [v1] credential has invalid number of parameters - {0}")] + InvalidNumberOfEmbededParameters(u32), + + // variant to catch legacy errors + #[error("{0}")] + Other(String), +} diff --git a/common/gateway-requests/src/types/helpers.rs b/common/gateway-requests/src/types/helpers.rs new file mode 100644 index 0000000000..f3d7ac3a1c --- /dev/null +++ b/common/gateway-requests/src/types/helpers.rs @@ -0,0 +1,133 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + BinaryRequest, BinaryRequestKind, BinaryResponse, BinaryResponseKind, GatewayRequestsError, + SharedGatewayKey, +}; +use std::iter::once; + +// each binary message consists of the following structure (for non-legacy messages) +// KIND || ENC_FLAG || MAYBE_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 +// finally, the rest of the message is the associated ciphertext or just plaintext (if message wasn't encrypted) +pub struct BinaryData<'a> { + kind: u8, + encrypted: bool, + maybe_nonce: Option<&'a [u8]>, + data: &'a [u8], +} + +impl<'a> BinaryData<'a> { + // serialises possibly encrypted data into bytes to be put on the wire + pub fn into_raw(self, legacy: bool) -> Vec { + if legacy { + return self.data.to_vec(); + } + + let i = once(self.kind).chain(once(if self.encrypted { 1 } else { 0 })); + if let Some(nonce) = self.maybe_nonce { + i.chain(nonce.iter().copied()) + .chain(self.data.iter().copied()) + .collect() + } else { + i.chain(self.data.iter().copied()).collect() + } + } + + // attempts to perform basic parsing on bytes received from the wire + pub fn from_raw( + raw: &'a [u8], + available_key: &SharedGatewayKey, + ) -> 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); + } + + let kind = raw[0]; + let encrypted = if raw[1] == 1 { + true + } else if raw[1] == 0 { + false + } else { + return Err(GatewayRequestsError::InvalidEncryptionFlag); + }; + + // if data is encrypted, there MUST be a nonce present for non-legacy keys + if encrypted && raw.len() < available_key.nonce_size() + 2 { + return Err(GatewayRequestsError::TooShortRequest); + } + + Ok(BinaryData { + kind, + encrypted, + maybe_nonce: Some(&raw[2..2 + available_key.nonce_size()]), + data: &raw[2 + available_key.nonce_size()..], + }) + } + + // attempt to encrypt plaintext of provided response/request and serialise it into wire format + pub fn make_encrypted_blob( + kind: u8, + plaintext: &[u8], + key: &SharedGatewayKey, + ) -> Result, GatewayRequestsError> { + let maybe_nonce = key.random_nonce_or_zero_iv(); + + let ciphertext = key.encrypt(plaintext, maybe_nonce.as_deref())?; + Ok(BinaryData { + kind, + encrypted: true, + maybe_nonce: maybe_nonce.as_deref(), + data: &ciphertext, + } + .into_raw(key.is_legacy())) + } + + // attempts to parse previously recovered bytes into a [`BinaryRequest`] + pub fn into_request( + self, + key: &SharedGatewayKey, + ) -> 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)? + } else { + self.data + }; + + BinaryRequest::from_plaintext(kind, plaintext) + } + + // attempts to parse previously recovered bytes into a [`BinaryResponse`] + pub fn into_response( + self, + key: &SharedGatewayKey, + ) -> 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)? + } else { + self.data + }; + + BinaryResponse::from_plaintext(kind, plaintext) + } +} diff --git a/common/gateway-requests/src/types/mod.rs b/common/gateway-requests/src/types/mod.rs new file mode 100644 index 0000000000..427ece4c7f --- /dev/null +++ b/common/gateway-requests/src/types/mod.rs @@ -0,0 +1,18 @@ +// Copyright 2020-2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod binary_request; +pub mod binary_response; +pub mod error; +mod helpers; +pub mod registration_handshake_wrapper; +pub mod text_request; +pub mod text_response; + +// just to preserve existing imports +pub use binary_request::*; +pub use binary_response::*; +pub use error::*; +pub use registration_handshake_wrapper::*; +pub use text_request::*; +pub use text_response::*; diff --git a/common/gateway-requests/src/types/registration_handshake_wrapper.rs b/common/gateway-requests/src/types/registration_handshake_wrapper.rs new file mode 100644 index 0000000000..0dc6daa567 --- /dev/null +++ b/common/gateway-requests/src/types/registration_handshake_wrapper.rs @@ -0,0 +1,103 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum RegistrationHandshake { + HandshakePayload { + #[serde(default)] + protocol_version: Option, + data: Vec, + }, + HandshakeError { + message: String, + }, +} + +impl RegistrationHandshake { + pub fn new_payload(data: Vec, protocol_version: u8) -> Self { + RegistrationHandshake::HandshakePayload { + protocol_version: Some(protocol_version), + data, + } + } + + pub fn new_error>(message: S) -> Self { + RegistrationHandshake::HandshakeError { + message: message.into(), + } + } +} + +impl FromStr for RegistrationHandshake { + type Err = serde_json::Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s) + } +} + +impl TryFrom for RegistrationHandshake { + type Error = serde_json::Error; + + fn try_from(msg: String) -> Result { + msg.parse() + } +} + +impl TryInto for RegistrationHandshake { + type Error = serde_json::Error; + + fn try_into(self) -> Result { + serde_json::to_string(&self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ClientControlRequest; + + #[test] + 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), + data: handshake_data.clone(), + }; + let serialized = serde_json::to_string(&handshake_payload_with_protocol).unwrap(); + let deserialized = ClientControlRequest::try_from(serialized).unwrap(); + + match deserialized { + ClientControlRequest::RegisterHandshakeInitRequest { + protocol_version, + data, + } => { + assert_eq!(protocol_version, Some(42)); + assert_eq!(data, handshake_data) + } + _ => unreachable!("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!(data, handshake_data) + } + _ => unreachable!("this branch shouldn't have been reached!"), + } + } +} diff --git a/common/gateway-requests/src/types/text_request.rs b/common/gateway-requests/src/types/text_request.rs new file mode 100644 index 0000000000..e03c211642 --- /dev/null +++ b/common/gateway-requests/src/types/text_request.rs @@ -0,0 +1,197 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::models::CredentialSpendingRequest; +use crate::{ + GatewayRequestsError, SharedGatewayKey, SymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION, + CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, +}; +use nym_credentials_interface::CredentialSpendingData; +use nym_sphinx::DestinationAddressBytes; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use tungstenite::Message; + +// wrapper for all encrypted requests for ease of use +#[derive(Serialize, Deserialize, Debug)] +pub enum ClientRequest { + UpgradeKey { + hkdf_salt: Vec, + derived_key_digest: Vec, + }, +} + +impl ClientRequest { + pub fn encrypt( + &self, + key: &S, + ) -> Result { + // we're using json representation for few reasons: + // - ease of re-implementation in other languages (compared to for example bincode) + // - we expect all requests to be relatively small - for anything bigger use BinaryRequest! + // - the schema is self-describing which simplifies deserialisation + + // SAFETY: the trait has been derived correctly with no weird variants + 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 }) + } + + pub fn decrypt( + ciphertext: &[u8], + nonce: &[u8], + key: &S, + ) -> Result { + let plaintext = key.decrypt(ciphertext, Some(nonce))?; + serde_json::from_slice(&plaintext) + .map_err(|source| GatewayRequestsError::MalformedRequest { source }) + } +} + +// if you're adding new variants here, consider putting them inside `ClientRequest` instead +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type", rename_all = "camelCase")] +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, + }, + #[serde(alias = "handshakePayload")] + RegisterHandshakeInitRequest { + #[serde(default)] + protocol_version: Option, + data: Vec, + }, + BandwidthCredential { + enc_credential: Vec, + iv: Vec, + }, + BandwidthCredentialV2 { + enc_credential: Vec, + iv: Vec, + }, + EcashCredential { + enc_credential: Vec, + iv: Vec, + }, + ClaimFreeTestnetBandwidth, + EncryptedRequest { + ciphertext: Vec, + nonce: Vec, + }, + SupportedProtocol {}, + // if you're adding new variants here, consider putting them inside `ClientRequest` instead +} + +impl ClientControlRequest { + pub fn new_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 + 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 name(&self) -> String { + match self { + ClientControlRequest::Authenticate { .. } => "Authenticate".to_string(), + ClientControlRequest::RegisterHandshakeInitRequest { .. } => { + "RegisterHandshakeInitRequest".to_string() + } + ClientControlRequest::BandwidthCredential { .. } => "BandwidthCredential".to_string(), + ClientControlRequest::BandwidthCredentialV2 { .. } => { + "BandwidthCredentialV2".to_string() + } + ClientControlRequest::EcashCredential { .. } => "EcashCredential".to_string(), + ClientControlRequest::ClaimFreeTestnetBandwidth => { + "ClaimFreeTestnetBandwidth".to_string() + } + ClientControlRequest::SupportedProtocol { .. } => "SupportedProtocol".to_string(), + ClientControlRequest::EncryptedRequest { .. } => "EncryptedRequest".to_string(), + } + } + + pub fn new_enc_ecash_credential( + credential: CredentialSpendingData, + shared_key: &SharedGatewayKey, + ) -> 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))?; + + Ok(ClientControlRequest::EcashCredential { + enc_credential, + iv: nonce, + }) + } + + pub fn try_from_enc_ecash_credential( + enc_credential: Vec, + shared_key: &SharedGatewayKey, + iv: Vec, + ) -> Result { + let credential_bytes = shared_key.decrypt(&enc_credential, Some(&iv))?; + CredentialSpendingRequest::try_from_bytes(credential_bytes.as_slice()) + .map_err(|_| GatewayRequestsError::MalformedEncryption) + } +} + +impl From for Message { + fn from(req: ClientControlRequest) -> Self { + // it should be safe to call `unwrap` here as the message is generated by the server + // so if it fails (and consequently panics) it's a bug that should be resolved + let str_req = serde_json::to_string(&req).unwrap(); + Message::Text(str_req) + } +} + +impl TryFrom for ClientControlRequest { + type Error = serde_json::Error; + + fn try_from(msg: String) -> Result { + msg.parse() + } +} + +impl FromStr for ClientControlRequest { + type Err = serde_json::Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s) + } +} + +impl TryInto for ClientControlRequest { + type Error = serde_json::Error; + + fn try_into(self) -> Result { + serde_json::to_string(&self) + } +} diff --git a/common/gateway-requests/src/types/text_response.rs b/common/gateway-requests/src/types/text_response.rs new file mode 100644 index 0000000000..e62d6e3745 --- /dev/null +++ b/common/gateway-requests/src/types/text_response.rs @@ -0,0 +1,128 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{GatewayRequestsError, SimpleGatewayRequestsError, SymmetricKey}; +use serde::{Deserialize, Serialize}; +use tungstenite::Message; + +// naming things is difficult... +// the name implies that the content is encrypted before being sent on the wire +#[derive(Serialize, Deserialize, Debug)] +pub enum SensitiveServerResponse { + KeyUpgradeAck {}, +} + +impl SensitiveServerResponse { + pub fn encrypt( + &self, + key: &S, + ) -> Result { + // we're using json representation for few reasons: + // - ease of re-implementation in other languages (compared to for example bincode) + // - we expect all requests to be relatively small - for anything bigger use BinaryRequest! + // - the schema is self-describing which simplifies deserialisation + + // SAFETY: the trait has been derived correctly with no weird variants + 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 }) + } + + pub fn decrypt( + ciphertext: &[u8], + nonce: &[u8], + key: &S, + ) -> Result { + let plaintext = key.decrypt(ciphertext, Some(nonce))?; + serde_json::from_slice(&plaintext) + .map_err(|source| GatewayRequestsError::MalformedRequest { source }) + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum ServerResponse { + Authenticate { + #[serde(default)] + protocol_version: Option, + status: bool, + bandwidth_remaining: i64, + }, + Register { + #[serde(default)] + protocol_version: Option, + status: bool, + }, + EncryptedResponse { + ciphertext: Vec, + nonce: Vec, + }, + Bandwidth { + available_total: i64, + }, + Send { + remaining_bandwidth: i64, + }, + SupportedProtocol { + version: u8, + }, + // Generic error + Error { + message: String, + }, + // Specific typed errors + // so that clients could match on this variant without doing naive string matching + TypedError { + error: SimpleGatewayRequestsError, + }, +} + +impl ServerResponse { + pub fn name(&self) -> String { + match self { + ServerResponse::Authenticate { .. } => "Authenticate".to_string(), + ServerResponse::Register { .. } => "Register".to_string(), + ServerResponse::Bandwidth { .. } => "Bandwidth".to_string(), + ServerResponse::Send { .. } => "Send".to_string(), + ServerResponse::Error { .. } => "Error".to_string(), + ServerResponse::TypedError { .. } => "TypedError".to_string(), + ServerResponse::SupportedProtocol { .. } => "SupportedProtocol".to_string(), + ServerResponse::EncryptedResponse { .. } => "EncryptedResponse".to_string(), + } + } + pub fn new_error>(msg: S) -> Self { + ServerResponse::Error { + message: msg.into(), + } + } + + pub fn is_error(&self) -> bool { + matches!(self, ServerResponse::Error { .. }) + } + + pub fn implies_successful_authentication(&self) -> bool { + match self { + ServerResponse::Authenticate { status, .. } => *status, + ServerResponse::Register { status, .. } => *status, + _ => false, + } + } +} + +impl From for Message { + fn from(res: ServerResponse) -> Self { + // it should be safe to call `unwrap` here as the message is generated by the server + // so if it fails (and consequently panics) it's a bug that should be resolved + let str_res = serde_json::to_string(&res).unwrap(); + Message::Text(str_res) + } +} + +impl TryFrom for ServerResponse { + type Error = serde_json::Error; + + fn try_from(msg: String) -> Result { + serde_json::from_str(&msg) + } +} From b3d7c26443f17ddb2c917e38c9d9a48a9f3c815b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 18 Sep 2024 17:37:14 +0100 Subject: [PATCH 14/17] added key upgrade mechanism --- Cargo.lock | 1 + .../src/backend/fs_backend/manager.rs | 29 +++- .../src/backend/fs_backend/mod.rs | 20 ++- .../src/backend/mem_backend.rs | 34 +++- .../client-core/gateways-storage/src/error.rs | 2 +- .../client-core/gateways-storage/src/lib.rs | 9 +- .../client-core/gateways-storage/src/types.rs | 4 +- .../client-core/src/client/base_client/mod.rs | 58 +++++-- .../base_client/storage/migration_helpers.rs | 2 +- .../src/client/base_client/storage/mod.rs | 36 +++- .../client-core/src/client/key_manager/mod.rs | 4 +- common/client-core/src/init/helpers.rs | 9 +- common/client-core/src/init/types.rs | 2 +- common/client-libs/gateway-client/Cargo.toml | 1 + .../gateway-client/src/client/mod.rs | 157 +++++++++++++----- .../client-libs/gateway-client/src/error.rs | 13 +- common/client-libs/gateway-client/src/lib.rs | 11 +- .../gateway-client/src/socket_state.rs | 2 +- .../gateway-requests/src/shared_key/legacy.rs | 23 +++ common/gateway-requests/src/shared_key/mod.rs | 7 +- .../src/types/binary_request.rs | 1 + .../src/types/binary_response.rs | 1 + .../src/types/text_request.rs | 2 + .../src/types/text_response.rs | 2 + common/gateway-storage/src/lib.rs | 2 +- common/gateway-storage/src/models.rs | 4 +- .../connection_handler/authenticated.rs | 70 +++++++- .../websocket/connection_handler/fresh.rs | 10 +- .../websocket/connection_handler/mod.rs | 4 +- nym-api/src/network_monitor/monitor/sender.rs | 4 + wasm/node-tester/src/tester.rs | 11 +- 31 files changed, 444 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 301e93c57e..2704c153ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4997,6 +4997,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-utils", "wasmtimer", + "zeroize", ] [[package]] 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 daa342fe9b..1bf78408f7 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 @@ -155,11 +155,12 @@ impl StorageManager { ) -> Result<(), sqlx::Error> { sqlx::query!( r#" - INSERT INTO remote_gateway_details(gateway_id_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58, gateway_owner_address, gateway_listener) - VALUES (?, ?, ?, ?) + 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 (?, ?, ?, ?, ?) "#, 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, ) @@ -168,6 +169,30 @@ impl StorageManager { 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 = ? + "#, + derived_aes128_ctr_blake3_hmac_keys_bs58, + derived_aes256_gcm_siv_key, + gateway_id_bs58 + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + pub(crate) async fn remove_remote_gateway_details( &self, gateway_id: &str, 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 0b93d284f6..c83a5bbd68 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 @@ -7,7 +7,8 @@ use crate::{ }; use async_trait::async_trait; use manager::StorageManager; -use nym_crypto::asymmetric::identity::PublicKey; +use nym_crypto::asymmetric::ed25519; +use nym_gateway_requests::SharedSymmetricKey; use std::path::Path; pub mod error; @@ -67,7 +68,7 @@ impl GatewaysDetailsStore for OnDiskGatewaysDetails { Ok(registered) } - async fn all_gateways_identities(&self) -> Result, Self::StorageError> { + async fn all_gateways_identities(&self) -> Result, Self::StorageError> { Ok(self .manager .registered_gateways() @@ -132,6 +133,21 @@ 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/mem_backend.rs b/common/client-core/gateways-storage/src/backend/mem_backend.rs index 451f6b2187..14fee876db 100644 --- a/common/client-core/gateways-storage/src/backend/mem_backend.rs +++ b/common/client-core/gateways-storage/src/backend/mem_backend.rs @@ -2,8 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 use crate::types::{ActiveGateway, GatewayRegistration}; -use crate::{BadGateway, GatewaysDetailsStore}; +use crate::{BadGateway, GatewayDetails, 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; @@ -34,10 +36,6 @@ struct InMemStorageInner { impl GatewaysDetailsStore for InMemGatewaysDetails { type StorageError = InMemStorageError; - async fn has_gateway_details(&self, gateway_id: &str) -> Result { - Ok(self.inner.read().await.gateways.contains_key(gateway_id)) - } - async fn active_gateway(&self) -> Result { let guard = self.inner.read().await; @@ -68,6 +66,10 @@ impl GatewaysDetailsStore for InMemGatewaysDetails { Ok(self.inner.read().await.gateways.values().cloned().collect()) } + async fn has_gateway_details(&self, gateway_id: &str) -> Result { + Ok(self.inner.read().await.gateways.contains_key(gateway_id)) + } + async fn load_gateway_details( &self, gateway_id: &str, @@ -94,6 +96,28 @@ 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; + + 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 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 effac347fb..82afc5334b 100644 --- a/common/client-core/gateways-storage/src/error.rs +++ b/common/client-core/gateways-storage/src/error.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use nym_crypto::asymmetric::identity::Ed25519RecoveryError; -use nym_gateway_requests::registration::handshake::SharedKeyConversionError; +use nym_gateway_requests::shared_key::SharedKeyConversionError; use thiserror::Error; #[derive(Debug, Error)] diff --git a/common/client-core/gateways-storage/src/lib.rs b/common/client-core/gateways-storage/src/lib.rs index 2d4f18422f..1351059afd 100644 --- a/common/client-core/gateways-storage/src/lib.rs +++ b/common/client-core/gateways-storage/src/lib.rs @@ -5,6 +5,8 @@ #![warn(clippy::unwrap_used)] use async_trait::async_trait; +use nym_crypto::asymmetric::identity; +use nym_gateway_requests::SharedSymmetricKey; use std::error::Error; pub mod backend; @@ -18,7 +20,6 @@ pub use error::BadGateway; #[cfg(all(not(target_arch = "wasm32"), feature = "fs-gateways-storage"))] pub use backend::fs_backend::{error::StorageError, OnDiskGatewaysDetails}; -use nym_crypto::asymmetric::identity; #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -61,6 +62,12 @@ pub trait GatewaysDetailsStore { details: &GatewayRegistration, ) -> Result<(), Self::StorageError>; + async fn upgrade_stored_remote_gateway_key( + &self, + gateway_id: identity::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 76cb2ae5ff..4f5f17843d 100644 --- a/common/client-core/gateways-storage/src/types.rs +++ b/common/client-core/gateways-storage/src/types.rs @@ -4,9 +4,7 @@ use crate::BadGateway; use cosmrs::AccountId; use nym_crypto::asymmetric::identity; -use nym_gateway_requests::registration::handshake::{ - LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey, -}; +use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey}; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::ops::Deref; diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index ba2b20c8b4..52e4680944 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -354,12 +354,14 @@ where config: &Config, initialisation_result: InitialisationResult, bandwidth_controller: Option>, + details_store: &S::GatewaysDetailsStore, packet_router: PacketRouter, shutdown: TaskClient, ) -> Result, ClientCoreError> where ::StorageError: Send + Sync + 'static, ::StorageError: Send + Sync + 'static, + ::StorageError: Sync + Send, { let managed_keys = initialisation_result.client_keys; let GatewayDetails::Remote(details) = initialisation_result.gateway_registration.details @@ -394,16 +396,47 @@ where ) }; + let gateway_failure = |err| { + log::error!("Could not authenticate and start up the gateway connection - {err}"); + ClientCoreError::GatewayClientError { + gateway_id: details.gateway_id.to_base58_string(), + source: err, + } + }; + + // the gateway client startup procedure is slightly more complicated now + // we need to: + // - perform handshake (reg or auth) + // - check for key upgrade + // - maybe perform another upgrade handshake + // - check for bandwidth + // - start background tasks + let auth_res = gateway_client + .perform_initial_authentication() + .await + .map_err(gateway_failure)?; + + if auth_res.requires_key_upgrade { + 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| { + error!("failed to store upgraded gateway key! this connection might be forever broken now: {err}"); + ClientCoreError::GatewaysDetailsStoreError { source: Box::new(err) } + })? + } + gateway_client - .authenticate_and_start() + .claim_initial_bandwidth() .await - .map_err(|err| { - log::error!("Could not authenticate and start up the gateway connection - {err}"); - ClientCoreError::GatewayClientError { - gateway_id: details.gateway_id.to_base58_string(), - source: err, - } - })?; + .map_err(gateway_failure)?; + gateway_client + .start_listening_for_mixnet_messages() + .map_err(gateway_failure)?; Ok(gateway_client) } @@ -413,12 +446,14 @@ where config: &Config, initialisation_result: InitialisationResult, bandwidth_controller: Option>, + details_store: &S::GatewaysDetailsStore, packet_router: PacketRouter, mut shutdown: TaskClient, ) -> Result, ClientCoreError> where ::StorageError: Send + Sync + 'static, ::StorageError: Send + Sync + 'static, + ::StorageError: Sync + Send, { // if we have setup custom gateway sender and persisted details agree with it, return it if let Some(mut custom_gateway_transceiver) = custom_gateway_transceiver { @@ -429,7 +464,7 @@ where { Err(ClientCoreError::CustomGatewaySelectionExpected) } else { - // and make sure to invalidate the task client so we wouldn't cause premature shutdown + // and make sure to invalidate the task client, so we wouldn't cause premature shutdown shutdown.disarm(); custom_gateway_transceiver.set_packet_router(packet_router)?; Ok(custom_gateway_transceiver) @@ -441,6 +476,7 @@ where config, initialisation_result, bandwidth_controller, + details_store, packet_router, shutdown, ) @@ -630,7 +666,8 @@ where ) .await?; - let (reply_storage_backend, credential_store) = self.client_store.into_runtime_stores(); + let (reply_storage_backend, credential_store, details_store) = + self.client_store.into_runtime_stores(); // channels for inter-component communication // TODO: make the channels be internally created by the relevant components @@ -705,6 +742,7 @@ where self.config, init_res, bandwidth_controller, + &details_store, gateway_packet_router, shutdown.fork("gateway_transceiver"), ) 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 61c5f59f1d..3ac62132f0 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 @@ -13,7 +13,7 @@ pub mod v1_1_33 { use nym_client_core_gateways_storage::{ CustomGatewayDetails, GatewayDetails, GatewayRegistration, RemoteGatewayDetails, }; - use nym_gateway_requests::registration::handshake::LegacySharedKeys; + use nym_gateway_requests::shared_key::LegacySharedKeys; use serde::{Deserialize, Serialize}; use sha2::{digest::Digest, Sha256}; use std::ops::Deref; diff --git a/common/client-core/src/client/base_client/storage/mod.rs b/common/client-core/src/client/base_client/storage/mod.rs index 8e325ddcd5..cccb3fa9d7 100644 --- a/common/client-core/src/client/base_client/storage/mod.rs +++ b/common/client-core/src/client/base_client/storage/mod.rs @@ -49,7 +49,13 @@ pub trait MixnetClientStorage { type CredentialStore: CredentialStorage; type GatewaysDetailsStore: GatewaysDetailsStore; - fn into_runtime_stores(self) -> (Self::ReplyStore, Self::CredentialStore); + fn into_runtime_stores( + self, + ) -> ( + Self::ReplyStore, + Self::CredentialStore, + Self::GatewaysDetailsStore, + ); fn key_store(&self) -> &Self::KeyStore; fn reply_store(&self) -> &Self::ReplyStore; @@ -77,8 +83,18 @@ impl MixnetClientStorage for Ephemeral { type CredentialStore = EphemeralCredentialStorage; type GatewaysDetailsStore = InMemGatewaysDetails; - fn into_runtime_stores(self) -> (Self::ReplyStore, Self::CredentialStore) { - (self.reply_store, self.credential_store) + fn into_runtime_stores( + self, + ) -> ( + Self::ReplyStore, + Self::CredentialStore, + Self::GatewaysDetailsStore, + ) { + ( + self.reply_store, + self.credential_store, + self.gateway_details_store, + ) } fn key_store(&self) -> &Self::KeyStore { @@ -168,8 +184,18 @@ impl MixnetClientStorage for OnDiskPersistent { type CredentialStore = PersistentCredentialStorage; type GatewaysDetailsStore = OnDiskGatewaysDetails; - fn into_runtime_stores(self) -> (Self::ReplyStore, Self::CredentialStore) { - (self.reply_store, self.credential_store) + fn into_runtime_stores( + self, + ) -> ( + Self::ReplyStore, + Self::CredentialStore, + Self::GatewaysDetailsStore, + ) { + ( + self.reply_store, + self.credential_store, + self.gateway_details_store, + ) } fn key_store(&self) -> &Self::KeyStore { diff --git a/common/client-core/src/client/key_manager/mod.rs b/common/client-core/src/client/key_manager/mod.rs index 6054f571f7..e9af947117 100644 --- a/common/client-core/src/client/key_manager/mod.rs +++ b/common/client-core/src/client/key_manager/mod.rs @@ -3,9 +3,7 @@ use crate::client::key_manager::persistence::KeyStore; use nym_crypto::asymmetric::{encryption, identity}; -use nym_gateway_requests::registration::handshake::{ - LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey, -}; +use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey}; use nym_sphinx::acknowledgements::AckKey; use rand::{CryptoRng, RngCore}; use std::sync::Arc; diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index 7db4e3a01e..fbd8163ec4 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -320,7 +320,7 @@ pub(super) async fn register_with_gateway( source: err, } })?; - let shared_keys = gateway_client + let auth_response = gateway_client .perform_initial_authentication() .await .map_err(|err| { @@ -330,8 +330,13 @@ pub(super) async fn register_with_gateway( source: err, } })?; + + // we can ignore the authentication result because we have **REGISTERED** a fresh client + // (we didn't have a prior key to upgrade/authenticate with) + assert!(!auth_response.requires_key_upgrade); + Ok(RegistrationResult { - shared_keys, + shared_keys: auth_response.current_shared_key, authenticated_ephemeral_client: gateway_client, }) } diff --git a/common/client-core/src/init/types.rs b/common/client-core/src/init/types.rs index 2acf91311f..2ba78dd6d1 100644 --- a/common/client-core/src/init/types.rs +++ b/common/client-core/src/init/types.rs @@ -11,7 +11,7 @@ use nym_client_core_gateways_storage::{ }; use nym_crypto::asymmetric::identity; use nym_gateway_client::client::InitGatewayClient; -use nym_gateway_requests::registration::handshake::SharedGatewayKey; +use nym_gateway_requests::shared_key::SharedGatewayKey; use nym_sphinx::addressing::clients::Recipient; use nym_topology::gateway; use nym_validator_client::client::IdentityKey; diff --git a/common/client-libs/gateway-client/Cargo.toml b/common/client-libs/gateway-client/Cargo.toml index dc8bfad577..77b72456dd 100644 --- a/common/client-libs/gateway-client/Cargo.toml +++ b/common/client-libs/gateway-client/Cargo.toml @@ -18,6 +18,7 @@ rand = { workspace = true } tokio = { workspace = true, features = ["macros"] } si-scale = { workspace = true } time.workspace = true +zeroize.workspace = true # internal nym-bandwidth-controller = { path = "../../bandwidth-controller" } diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index 6cb784cdbf..4603892d4b 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -17,9 +17,10 @@ use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCred use nym_credential_storage::storage::Storage as CredentialStorage; use nym_credentials::CredentialSpendingData; use nym_crypto::asymmetric::identity; -use nym_gateway_requests::registration::handshake::{client_handshake, SharedGatewayKey}; +use nym_gateway_requests::registration::handshake::client_handshake; use nym_gateway_requests::{ - BinaryRequest, ClientControlRequest, ServerResponse, AES_GCM_SIV_PROTOCOL_VERSION, + BinaryRequest, ClientControlRequest, ClientRequest, SensitiveServerResponse, ServerResponse, + SharedGatewayKey, SharedSymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION, }; use nym_sphinx::forwarding::packet::MixPacket; @@ -45,6 +46,7 @@ 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; @@ -71,6 +73,13 @@ impl GatewayConfig { } } +#[must_use] +#[derive(Debug)] +pub struct AuthenticationResponse { + pub current_shared_key: Arc, + pub requires_key_upgrade: bool, +} + // TODO: this should be refactored into a state machine that keeps track of its authentication state pub struct GatewayClient { pub cfg: GatewayClientConfig, @@ -440,7 +449,7 @@ impl GatewayClient { ServerResponse::Error { message } => { return Err(GatewayClientError::GatewayError(message)) } - _ => return Err(GatewayClientError::UnexpectedResponse), + other => return Err(GatewayClientError::UnexpectedResponse { name: other.name() }), }; self.check_gateway_protocol(gateway_protocol)?; @@ -456,18 +465,68 @@ impl GatewayClient { Ok(()) } - async fn upgrade_key_authenticated(&mut self) -> Result<(), GatewayClientError> { + pub async fn upgrade_key_authenticated( + &mut self, + ) -> Result, GatewayClientError> { + 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 _ = legacy_key; - warn!("unimplemented: migration into aes256gcm-siv key!"); + let upgrade_request = ClientRequest::UpgradeKey { + hkdf_salt, + derived_key_digest, + } + .encrypt(legacy_key)?; - Ok(()) + info!("sending upgrade request and awaiting the acknowledgement back"); + let (ciphertext, nonce) = match self.send_websocket_message(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 authenticate(&mut self) -> Result<(), GatewayClientError> { @@ -511,7 +570,7 @@ impl GatewayClient { Ok(()) } ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), - _ => Err(GatewayClientError::UnexpectedResponse), + other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }), } } @@ -524,7 +583,11 @@ impl GatewayClient { )] pub async fn perform_initial_authentication( &mut self, - ) -> Result, GatewayClientError> { + ) -> Result { + if !self.connection.is_established() { + self.establish_connection().await?; + } + // 1. check gateway's protocol version let supports_aes_gcm_siv = match self.get_gateway_protocol().await { Ok(protocol) => protocol >= AES_GCM_SIV_PROTOCOL_VERSION, @@ -543,7 +606,10 @@ impl GatewayClient { if self.authenticated { debug!("Already authenticated"); return if let Some(shared_key) = &self.shared_key { - Ok(Arc::clone(shared_key)) + Ok(AuthenticationResponse { + current_shared_key: Arc::clone(shared_key), + requires_key_upgrade: shared_key.is_legacy() && supports_aes_gcm_siv, + }) } else { Err(GatewayClientError::AuthenticationFailureWithPreexistingSharedKey) }; @@ -553,27 +619,30 @@ impl GatewayClient { self.authenticate().await?; if self.authenticated { - // if we managed to authenticate with an old key, see if we can upgrade it - if self - .shared_key - .as_ref() - .map(|k| k.is_legacy()) - .unwrap_or_default() - && supports_aes_gcm_siv - { - info!("using a legacy shared key and the gateway supports the updated format - attempting to upgrade"); - self.upgrade_key_authenticated().await?; - } + // if we are authenticated it means we MUST have an associated shared_key + let shared_key = self.shared_key.as_ref().unwrap(); + + let requires_key_upgrade = shared_key.is_legacy() && supports_aes_gcm_siv; + + Ok(AuthenticationResponse { + current_shared_key: Arc::clone(shared_key), + requires_key_upgrade, + }) + } else { + Err(GatewayClientError::AuthenticationFailure) } } else { self.register(supports_aes_gcm_siv).await?; - } - if self.authenticated { - // if we are authenticated it means we MUST have an associated shared_key - Ok(Arc::clone(self.shared_key.as_ref().unwrap())) - } else { - Err(GatewayClientError::AuthenticationFailure) + // if registration didn't return an error, we MUST have an associated shared key + let shared_key = self.shared_key.as_ref().unwrap(); + + // we're always registering with the highest supported protocol, + // so no upgrades are required + Ok(AuthenticationResponse { + current_shared_key: Arc::clone(shared_key), + requires_key_upgrade: false, + }) } } @@ -588,7 +657,7 @@ impl GatewayClient { { ServerResponse::SupportedProtocol { version } => Ok(version), ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), - _ => Err(GatewayClientError::UnexpectedResponse), + other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }), } } @@ -606,7 +675,7 @@ impl GatewayClient { ServerResponse::TypedError { error } => { Err(GatewayClientError::TypedGatewayError(error)) } - _ => Err(GatewayClientError::UnexpectedResponse), + other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }), }?; // TODO: create tracing span @@ -621,7 +690,7 @@ impl GatewayClient { let bandwidth_remaining = match self.send_websocket_message(msg).await? { ServerResponse::Bandwidth { available_total } => Ok(available_total), ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), - _ => Err(GatewayClientError::UnexpectedResponse), + other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }), }?; info!("managed to claim testnet bandwidth"); @@ -860,8 +929,8 @@ impl GatewayClient { self.establish_connection().await?; } - // TODO: the name of this method is very deceiving - self.perform_initial_authentication().await?; + // if we're reconnecting, because we lost connection, we need to re-authenticate the connection + self.authenticate().await?; // this call is NON-blocking self.start_listening_for_mixnet_messages()?; @@ -875,18 +944,16 @@ impl GatewayClient { Ok(()) } - pub async fn authenticate_and_start( - &mut self, - ) -> Result, GatewayClientError> + pub async fn claim_initial_bandwidth(&mut self) -> Result<(), GatewayClientError> where C: DkgQueryClient + Send + Sync, St: CredentialStorage, ::StorageError: Send + Sync + 'static, { - if !self.connection.is_established() { - self.establish_connection().await?; + if !self.authenticated { + return Err(GatewayClientError::NotAuthenticated); } - let shared_key = self.perform_initial_authentication().await?; + let bandwidth_remaining = self.bandwidth.remaining(); if bandwidth_remaining < self.cfg.bandwidth.remaining_bandwidth_threshold { self.cfg @@ -895,6 +962,20 @@ impl GatewayClient { info!("Claiming more bandwidth with existing credentials. Stop the process now if you don't want that to happen."); self.claim_bandwidth().await?; } + Ok(()) + } + + #[deprecated(note = "this method does not deal with upgraded keys for legacy clients")] + pub async fn authenticate_and_start( + &mut self, + ) -> Result + where + C: DkgQueryClient + Send + Sync, + St: CredentialStorage, + ::StorageError: Send + Sync + 'static, + { + let shared_key = self.perform_initial_authentication().await?; + self.claim_initial_bandwidth().await?; // this call is NON-blocking self.start_listening_for_mixnet_messages()?; diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index d796b6df71..e1b581d30e 100644 --- a/common/client-libs/gateway-client/src/error.rs +++ b/common/client-libs/gateway-client/src/error.rs @@ -27,6 +27,15 @@ pub enum GatewayClientError { #[error("There was a network error: {0}")] NetworkError(#[from] WsError), + #[error("failed to upgrade our shared key - the gateway sent malformed response")] + FatalKeyUpgradeFailure, + + #[error("the current key is already up to date! there's no need to upgrade it")] + KeyAlreadyUpgraded, + + #[error("can't perform key upgrade as the key is already being used elsewhere")] + KeyAlreadyInUse, + #[cfg(target_arch = "wasm32")] #[error("There was a network error: {0}")] NetworkErrorWasm(#[from] JsError), @@ -76,8 +85,8 @@ pub enum GatewayClientError { cutoff_bi2: String, }, - #[error("Received an unexpected response")] - UnexpectedResponse, + #[error("received an unexpected response of type {name}")] + UnexpectedResponse { name: String }, #[error("Connection is in an invalid state - please send a bug report")] ConnectionInInvalidState, diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index 2d58349409..e8d992027a 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -3,12 +3,13 @@ use crate::error::GatewayClientError; use nym_gateway_requests::BinaryResponse; -use tracing::warn; +use tracing::{error, warn}; use tungstenite::{protocol::Message, Error as WsError}; pub use client::{config::GatewayClientConfig, GatewayClient, GatewayConfig}; -pub use nym_gateway_requests::registration::handshake::LegacySharedKeys; -pub use nym_gateway_requests::registration::handshake::SharedGatewayKey; +pub use nym_gateway_requests::shared_key::{ + LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey, +}; pub use packet_router::{ AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender, PacketRouter, @@ -51,6 +52,10 @@ pub(crate) fn try_decrypt_binary_message( match BinaryResponse::try_from_encrypted_tagged_bytes(bin_msg, shared_keys) { Ok(bin_response) => match bin_response { BinaryResponse::PushedMixMessage { message } => Some(message), + _ => { + error!("received unhandled binary response"); + None + } }, Err(err) => { warn!("message received from the gateway was malformed! - {err}",); diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index b32f85d3f3..942f650614 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -9,7 +9,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::registration::handshake::SharedGatewayKey; +use nym_gateway_requests::shared_key::SharedGatewayKey; use nym_gateway_requests::{ServerResponse, SimpleGatewayRequestsError}; use nym_task::TaskClient; use si_scale::helpers::bibytes2; diff --git a/common/gateway-requests/src/shared_key/legacy.rs b/common/gateway-requests/src/shared_key/legacy.rs index 20820808f2..8fcf286697 100644 --- a/common/gateway-requests/src/shared_key/legacy.rs +++ b/common/gateway-requests/src/shared_key/legacy.rs @@ -56,6 +56,29 @@ impl LegacySharedKeys { (key, salt) } + pub fn upgrade_verify( + &self, + salt: &[u8], + expected_digest: &[u8], + ) -> Option { + let legacy_bytes = Zeroizing::new(self.to_bytes()); + let okm = hkdf::extract_then_expand::( + Some(salt), + &legacy_bytes, + None, + SharedKeySize::to_usize(), + ) + .expect("somehow too long okm was provided"); + 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 { diff --git a/common/gateway-requests/src/shared_key/mod.rs b/common/gateway-requests/src/shared_key/mod.rs index 009f43eca2..b424fd49bf 100644 --- a/common/gateway-requests/src/shared_key/mod.rs +++ b/common/gateway-requests/src/shared_key/mod.rs @@ -1,7 +1,6 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::LegacySharedKeys; use nym_crypto::blake3; use nym_crypto::crypto_hash::compute_digest; use nym_crypto::generic_array::{typenum::Unsigned, GenericArray}; @@ -16,6 +15,8 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; +pub use legacy::LegacySharedKeys; + pub mod helpers; pub mod legacy; @@ -240,6 +241,10 @@ impl SharedSymmetricKey { Ok(SharedSymmetricKey(GenericArray::clone_from_slice(bytes))) } + pub fn zeroizing_clone(&self) -> Zeroizing { + Zeroizing::new(SharedSymmetricKey(self.0)) + } + pub fn digest(&self) -> Vec { compute_digest::(self.as_bytes()).to_vec() } diff --git a/common/gateway-requests/src/types/binary_request.rs b/common/gateway-requests/src/types/binary_request.rs index 3b0f21233f..17eab6c9cb 100644 --- a/common/gateway-requests/src/types/binary_request.rs +++ b/common/gateway-requests/src/types/binary_request.rs @@ -8,6 +8,7 @@ use strum::FromRepr; use tungstenite::Message; // in legacy mode requests use zero IV without +#[non_exhaustive] pub enum BinaryRequest { ForwardSphinx { packet: MixPacket }, } diff --git a/common/gateway-requests/src/types/binary_response.rs b/common/gateway-requests/src/types/binary_response.rs index af1551f013..d3652b64e4 100644 --- a/common/gateway-requests/src/types/binary_response.rs +++ b/common/gateway-requests/src/types/binary_response.rs @@ -6,6 +6,7 @@ use crate::{GatewayRequestsError, SharedGatewayKey}; use strum::FromRepr; use tungstenite::Message; +#[non_exhaustive] pub enum BinaryResponse { PushedMixMessage { message: Vec }, } diff --git a/common/gateway-requests/src/types/text_request.rs b/common/gateway-requests/src/types/text_request.rs index e03c211642..8be56bcfb3 100644 --- a/common/gateway-requests/src/types/text_request.rs +++ b/common/gateway-requests/src/types/text_request.rs @@ -14,6 +14,7 @@ use tungstenite::Message; // wrapper for all encrypted requests for ease of use #[derive(Serialize, Deserialize, Debug)] +#[non_exhaustive] pub enum ClientRequest { UpgradeKey { hkdf_salt: Vec, @@ -52,6 +53,7 @@ impl ClientRequest { // if you're adding new variants here, consider putting them inside `ClientRequest` instead #[derive(Serialize, Deserialize, Debug)] #[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? diff --git a/common/gateway-requests/src/types/text_response.rs b/common/gateway-requests/src/types/text_response.rs index e62d6e3745..b0c8250f1e 100644 --- a/common/gateway-requests/src/types/text_response.rs +++ b/common/gateway-requests/src/types/text_response.rs @@ -8,6 +8,7 @@ use tungstenite::Message; // naming things is difficult... // the name implies that the content is encrypted before being sent on the wire #[derive(Serialize, Deserialize, Debug)] +#[non_exhaustive] pub enum SensitiveServerResponse { KeyUpgradeAck {}, } @@ -42,6 +43,7 @@ impl SensitiveServerResponse { #[derive(Serialize, Deserialize, Debug)] #[serde(tag = "type", rename_all = "camelCase")] +#[non_exhaustive] pub enum ServerResponse { Authenticate { #[serde(default)] diff --git a/common/gateway-storage/src/lib.rs b/common/gateway-storage/src/lib.rs index 4bae9568aa..3c3e5472d9 100644 --- a/common/gateway-storage/src/lib.rs +++ b/common/gateway-storage/src/lib.rs @@ -11,7 +11,7 @@ use models::{ VerifiedTicket, WireguardPeer, }; use nym_credentials_interface::ClientTicket; -use nym_gateway_requests::registration::handshake::SharedGatewayKey; +use nym_gateway_requests::shared_key::SharedGatewayKey; use nym_sphinx::DestinationAddressBytes; use shared_keys::SharedKeysManager; use sqlx::ConnectOptions; diff --git a/common/gateway-storage/src/models.rs b/common/gateway-storage/src/models.rs index fac26852da..70b6c407c1 100644 --- a/common/gateway-storage/src/models.rs +++ b/common/gateway-storage/src/models.rs @@ -3,9 +3,7 @@ use crate::error::StorageError; use nym_credentials_interface::{AvailableBandwidth, ClientTicket, CredentialSpendingData}; -use nym_gateway_requests::registration::handshake::{ - LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey, -}; +use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey}; use sqlx::FromRow; use time::OffsetDateTime; 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 8c6cb2ef29..b785e7bdb5 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -20,7 +20,8 @@ use nym_credential_verification::{ }; use nym_gateway_requests::{ types::{BinaryRequest, ServerResponse}, - ClientControlRequest, GatewayRequestsError, SimpleGatewayRequestsError, + ClientControlRequest, ClientRequest, GatewayRequestsError, SensitiveServerResponse, + SimpleGatewayRequestsError, }; use nym_gateway_storage::{error::StorageError, Storage}; use nym_sphinx::forwarding::packet::MixPacket; @@ -43,9 +44,21 @@ pub enum RequestHandlingError { )] MissingClientBandwidthEntry { client_address: String }, + #[error("received a binary request of an unknown type")] + UnknownBinaryRequest, + + #[error("received a text request of an unknown type")] + UnknownTextRequest, + + #[error("received an encrypted text request of an unknown type")] + UnknownEncryptedTextRequest, + #[error("Provided binary request was malformed - {0}")] InvalidBinaryRequest(#[from] GatewayRequestsError), + #[error("failed to decrypt provided text request")] + InvalidEncryptedTextRequest, + #[error("Provided binary request was malformed - {0}")] InvalidTextRequest(>::Error), @@ -289,10 +302,61 @@ where BinaryRequest::ForwardSphinx { packet } => { self.handle_forward_sphinx(packet).await.into_ws_message() } + _ => RequestHandlingError::UnknownBinaryRequest.into_error_message(), }, } } + 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, + nonce: Vec, + ) -> Message { + let Ok(req) = ClientRequest::decrypt(&ciphertext, &nonce, &self.client.shared_keys) else { + return RequestHandlingError::InvalidEncryptedTextRequest.into_error_message(); + }; + + match req { + ClientRequest::UpgradeKey { + hkdf_salt, + derived_key_digest, + } => self + .handle_key_upgrade(hkdf_salt, derived_key_digest) + .await + .into_ws_message(), + _ => RequestHandlingError::UnknownEncryptedTextRequest.into_error_message(), + } + } + /// Attempts to handle a text data frame websocket message. /// /// Currently the bandwidth credential request is the only one we can receive after authentication. @@ -305,6 +369,9 @@ where match ClientControlRequest::try_from(raw_request) { Err(e) => RequestHandlingError::InvalidTextRequest(e).into_error_message(), Ok(request) => match request { + 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 @@ -349,6 +416,7 @@ where } .into_error_message() } + _ => RequestHandlingError::UnknownTextRequest.into_error_message(), }, } } 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 8b96a5bda0..ddc266342f 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -17,14 +17,15 @@ use futures::{ SinkExt, StreamExt, }; use nym_credentials_interface::AvailableBandwidth; +use nym_crypto::aes::cipher::crypto_common::rand_core::RngCore; use nym_crypto::asymmetric::identity; use nym_gateway_requests::authentication::encrypted_address::{ EncryptedAddressBytes, EncryptedAddressConversionError, }; use nym_gateway_requests::{ - registration::handshake::{error::HandshakeError, gateway_handshake, SharedGatewayKey}, + registration::handshake::{error::HandshakeError, gateway_handshake}, types::{ClientControlRequest, ServerResponse}, - BinaryResponse, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, + BinaryResponse, SharedGatewayKey, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, }; use nym_gateway_storage::{error::StorageError, Storage}; use nym_mixnet_client::forwarder::MixForwardingSender; @@ -178,6 +179,7 @@ where ) -> Result where S: AsyncRead + AsyncWrite + Unpin + Send, + R: CryptoRng + RngCore + Send, { debug_assert!(self.socket_connection.is_websocket()); match &mut self.socket_connection { @@ -662,6 +664,7 @@ where ) -> Result where S: AsyncRead + AsyncWrite + Unpin + Send, + R: CryptoRng + RngCore + Send, { let negotiated_protocol = self.negotiate_client_protocol(client_protocol_version)?; // populate the negotiated protocol for future uses @@ -717,6 +720,7 @@ where ) -> Result, InitialAuthenticationError> where S: AsyncRead + AsyncWrite + Unpin + Send, + R: CryptoRng + RngCore + Send, { // we can handle stateless client requests without prior authentication, like `ClientControlRequest::SupportedProtocol` let auth_result = match request { @@ -782,6 +786,7 @@ where ) -> Option> where S: AsyncRead + AsyncWrite + Unpin + Send, + R: CryptoRng + RngCore + Send, { while !shutdown.is_shutdown() { let req = tokio::select! { @@ -868,6 +873,7 @@ where pub(crate) async fn start_handling(self) where S: AsyncRead + AsyncWrite + Unpin + Send, + R: CryptoRng + RngCore + Send, { super::handle_connection(self).await } 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 bc9f47f4b8..caac41fba3 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::registration::handshake::SharedGatewayKey; +use nym_gateway_requests::shared_key::SharedGatewayKey; use nym_gateway_requests::ServerResponse; use nym_gateway_storage::Storage; use nym_sphinx::DestinationAddressBytes; @@ -92,7 +92,7 @@ impl InitialAuthResult { #[instrument(level = "debug", skip_all, fields(peer = %handle.peer_address))] pub(crate) async fn handle_connection(mut handle: FreshHandler) where - R: Rng + CryptoRng, + R: Rng + CryptoRng + Send, S: AsyncRead + AsyncWrite + Unpin + Send, St: Storage + Clone + 'static, { diff --git a/nym-api/src/network_monitor/monitor/sender.rs b/nym-api/src/network_monitor/monitor/sender.rs index 531bc7be8f..e2c5bf8e4d 100644 --- a/nym-api/src/network_monitor/monitor/sender.rs +++ b/nym-api/src/network_monitor/monitor/sender.rs @@ -301,6 +301,10 @@ impl PacketSender { // and it wasn't shared with anyone, therefore we're the only one holding reference to it // and hence it's impossible to fail to obtain the permit. let mut unlocked_client = new_client.lock_client_unchecked(); + + // SAFETY: it's fine to use the deprecated method here as we're creating brand new clients each time, + // and there's no need to deal with any key upgrades + #[allow(deprecated)] match tokio::time::timeout( gateway_connection_timeout, unlocked_client.get_mut_unchecked().authenticate_and_start(), diff --git a/wasm/node-tester/src/tester.rs b/wasm/node-tester/src/tester.rs index ef35acf759..43ff55d7bf 100644 --- a/wasm/node-tester/src/tester.rs +++ b/wasm/node-tester/src/tester.rs @@ -26,6 +26,7 @@ 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, @@ -212,7 +213,15 @@ impl NymNodeTesterBuilder { ) }; - gateway_client.authenticate_and_start().await?; + 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?; + } + gateway_client.claim_initial_bandwidth().await?; + gateway_client.start_listening_for_mixnet_messages().await?; // TODO: make those values configurable later let tester = NodeTester::new( From 7c0235ab26b4cb2fa724a2c6ce39211a287d06e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 19 Sep 2024 10:06:59 +0100 Subject: [PATCH 15/17] fixed wasm build and trait impl --- .../src/backend/mem_backend.rs | 3 +- .../src/registration/handshake/messages.rs | 3 +- .../src/storage/core_client_traits.rs | 29 ++++++++++++++++-- common/wasm/client-core/src/storage/types.rs | 4 ++- .../src/storage/wasm_client_traits.rs | 18 +++++++++++ nym-wallet/Cargo.lock | 30 ++++++++++++++++--- sdk/lib/socks5-listener/src/persistence.rs | 14 +++++++-- .../examples/manually_handle_storage.rs | 26 ++++++++++++++-- wasm/node-tester/src/tester.rs | 2 +- 9 files changed, 115 insertions(+), 14 deletions(-) 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 14fee876db..70560995e9 100644 --- a/common/client-core/gateways-storage/src/backend/mem_backend.rs +++ b/common/client-core/gateways-storage/src/backend/mem_backend.rs @@ -103,13 +103,14 @@ impl GatewaysDetailsStore for InMemGatewaysDetails { ) -> 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 for ephemeral clients so should be fine for now... + // 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(), )) diff --git a/common/gateway-requests/src/registration/handshake/messages.rs b/common/gateway-requests/src/registration/handshake/messages.rs index 6f64c561b0..10dda7edaf 100644 --- a/common/gateway-requests/src/registration/handshake/messages.rs +++ b/common/gateway-requests/src/registration/handshake/messages.rs @@ -25,6 +25,7 @@ pub struct Initialisation { } impl Initialisation { + #[cfg(not(target_arch = "wasm32"))] pub fn is_legacy(&self) -> bool { self.initiator_salt.is_none() } @@ -78,7 +79,7 @@ impl HandshakeMessage for Initialisation { .chain(self.ephemeral_dh.to_bytes()); if let Some(salt) = self.initiator_salt { - bytes.chain(salt.into_iter()).collect() + bytes.chain(salt).collect() } else { bytes.collect() } 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 20105ac339..3824b6e9be 100644 --- a/common/wasm/client-core/src/storage/core_client_traits.rs +++ b/common/wasm/client-core/src/storage/core_client_traits.rs @@ -15,6 +15,8 @@ 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` @@ -43,8 +45,18 @@ impl MixnetClientStorage for FullWasmClientStorage { type GatewaysDetailsStore = ClientStorage; - fn into_runtime_stores(self) -> (Self::ReplyStore, Self::CredentialStore) { - (self.reply_storage, self.credential_storage) + fn into_runtime_stores( + self, + ) -> ( + Self::ReplyStore, + Self::CredentialStore, + Self::GatewaysDetailsStore, + ) { + ( + self.reply_storage, + self.credential_storage, + self.keys_and_gateway_store, + ) } fn key_store(&self) -> &Self::KeyStore { @@ -142,6 +154,19 @@ 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 c59d729589..b94f04f651 100644 --- a/common/wasm/client-core/src/storage/types.rs +++ b/common/wasm/client-core/src/storage/types.rs @@ -8,12 +8,14 @@ use nym_gateway_client::SharedGatewayKey; use serde::{Deserialize, Serialize}; use std::ops::Deref; use time::OffsetDateTime; +use zeroize::Zeroize; // a more nested struct since we only have a single gateway type in wasm (no 'custom') -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Zeroize)] pub struct WasmRawRegisteredGateway { pub gateway_id_bs58: String, + #[zeroize(skip)] pub registration_timestamp: OffsetDateTime, pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option, 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 7c1b8e3e02..677056498e 100644 --- a/common/wasm/client-core/src/storage/wasm_client_traits.rs +++ b/common/wasm/client-core/src/storage/wasm_client_traits.rs @@ -10,6 +10,7 @@ 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 { @@ -228,6 +229,23 @@ 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/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index e4e79726d3..f949feda83 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -1930,14 +1930,14 @@ dependencies = [ [[package]] name = "getset" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" +checksum = "f636605b743120a8d32ed92fc27b6cde1a769f8f936c065151eb66f88ded513c" dependencies = [ - "proc-macro-error", + "proc-macro-error2", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.55", ] [[package]] @@ -4130,6 +4130,28 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.55", +] + [[package]] name = "proc-macro-hack" version = "0.5.20+deprecated" diff --git a/sdk/lib/socks5-listener/src/persistence.rs b/sdk/lib/socks5-listener/src/persistence.rs index 9ee4990e6c..77a903144a 100644 --- a/sdk/lib/socks5-listener/src/persistence.rs +++ b/sdk/lib/socks5-listener/src/persistence.rs @@ -22,8 +22,18 @@ impl MixnetClientStorage for MobileClientStorage { type CredentialStore = EphemeralCredentialStorage; type GatewaysDetailsStore = InMemGatewaysDetails; - fn into_runtime_stores(self) -> (Self::ReplyStore, Self::CredentialStore) { - (self.reply_store, self.credential_store) + fn into_runtime_stores( + self, + ) -> ( + Self::ReplyStore, + Self::CredentialStore, + Self::GatewaysDetailsStore, + ) { + ( + self.reply_store, + self.credential_store, + self.gateway_details_store, + ) } fn key_store(&self) -> &Self::KeyStore { diff --git a/sdk/rust/nym-sdk/examples/manually_handle_storage.rs b/sdk/rust/nym-sdk/examples/manually_handle_storage.rs index 298502458c..a7fb182743 100644 --- a/sdk/rust/nym-sdk/examples/manually_handle_storage.rs +++ b/sdk/rust/nym-sdk/examples/manually_handle_storage.rs @@ -1,3 +1,5 @@ +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, @@ -63,8 +65,18 @@ impl MixnetClientStorage for MockClientStorage { type CredentialStore = EphemeralCredentialStorage; type GatewaysDetailsStore = MockGatewayDetailsStore; - fn into_runtime_stores(self) -> (Self::ReplyStore, Self::CredentialStore) { - (self.reply_store, self.credential_store) + fn into_runtime_stores( + self, + ) -> ( + Self::ReplyStore, + Self::CredentialStore, + Self::GatewaysDetailsStore, + ) { + ( + self.reply_store, + self.credential_store, + self.gateway_details_store, + ) } fn key_store(&self) -> &Self::KeyStore { @@ -151,6 +163,16 @@ 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 43ff55d7bf..5d4958fa9f 100644 --- a/wasm/node-tester/src/tester.rs +++ b/wasm/node-tester/src/tester.rs @@ -221,7 +221,7 @@ impl NymNodeTesterBuilder { .await?; } gateway_client.claim_initial_bandwidth().await?; - gateway_client.start_listening_for_mixnet_messages().await?; + gateway_client.start_listening_for_mixnet_messages()?; // TODO: make those values configurable later let tester = NodeTester::new( From f5863b9668c611e1d805bd18f7f6d1d0841352c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 19 Sep 2024 11:06:50 +0100 Subject: [PATCH 16/17] fixed client key upgrade due to extra Arc --- common/client-core/src/client/base_client/mod.rs | 3 +++ common/client-core/src/init/helpers.rs | 2 +- common/client-libs/gateway-client/src/client/mod.rs | 10 ++++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index 52e4680944..646300d5aa 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -417,6 +417,9 @@ where .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 diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index fbd8163ec4..74298d0d6d 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -336,7 +336,7 @@ pub(super) async fn register_with_gateway( assert!(!auth_response.requires_key_upgrade); Ok(RegistrationResult { - shared_keys: auth_response.current_shared_key, + shared_keys: auth_response.initial_shared_key, authenticated_ephemeral_client: gateway_client, }) } diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index 4603892d4b..b1e343c68b 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -76,7 +76,7 @@ impl GatewayConfig { #[must_use] #[derive(Debug)] pub struct AuthenticationResponse { - pub current_shared_key: Arc, + pub initial_shared_key: Arc, pub requires_key_upgrade: bool, } @@ -468,6 +468,8 @@ impl GatewayClient { 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); } @@ -607,7 +609,7 @@ impl GatewayClient { debug!("Already authenticated"); return if let Some(shared_key) = &self.shared_key { Ok(AuthenticationResponse { - current_shared_key: Arc::clone(shared_key), + initial_shared_key: Arc::clone(shared_key), requires_key_upgrade: shared_key.is_legacy() && supports_aes_gcm_siv, }) } else { @@ -625,7 +627,7 @@ impl GatewayClient { let requires_key_upgrade = shared_key.is_legacy() && supports_aes_gcm_siv; Ok(AuthenticationResponse { - current_shared_key: Arc::clone(shared_key), + initial_shared_key: Arc::clone(shared_key), requires_key_upgrade, }) } else { @@ -640,7 +642,7 @@ impl GatewayClient { // we're always registering with the highest supported protocol, // so no upgrades are required Ok(AuthenticationResponse { - current_shared_key: Arc::clone(shared_key), + initial_shared_key: Arc::clone(shared_key), requires_key_upgrade: false, }) } From be7f00fe527d5dd096efef257997fb429f066e4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 19 Sep 2024 15:55:54 +0100 Subject: [PATCH 17/17] replaced an assertion with an error return instead --- common/client-core/src/error.rs | 5 +++++ common/client-core/src/init/helpers.rs | 10 +++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index cb9b9126a3..653256df1f 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -214,6 +214,11 @@ pub enum ClientCoreError { #[error("this client has already registered with gateway {gateway_id}")] AlreadyRegistered { gateway_id: String }, + + #[error( + "fresh registration with gateway {gateway_id} somehow requires an additional key upgrade!" + )] + UnexpectedKeyUpgrade { gateway_id: String }, } /// Set of messages that the client can send to listeners via the task manager diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index 74298d0d6d..c6d17fc16d 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -331,9 +331,13 @@ pub(super) async fn register_with_gateway( } })?; - // we can ignore the authentication result because we have **REGISTERED** a fresh client - // (we didn't have a prior key to upgrade/authenticate with) - assert!(!auth_response.requires_key_upgrade); + // 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,