feature: v2 authentication request (#5537)
* introduced v2 authentication request between clients and gateways * client to send v2 auth when possible * added persistence to last used authentication timestamp * added clients identity to signed plaintext
This commit is contained in:
committed by
GitHub
parent
2a6fe6624d
commit
155c4d37ef
Generated
+3
@@ -5835,13 +5835,16 @@ dependencies = [
|
||||
"nym-credentials-interface",
|
||||
"nym-crypto",
|
||||
"nym-pemstore",
|
||||
"nym-serde-helpers",
|
||||
"nym-sphinx",
|
||||
"nym-task",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.26.3",
|
||||
"subtle 2.6.1",
|
||||
"thiserror 2.0.11",
|
||||
"time",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tungstenite 0.20.1",
|
||||
|
||||
@@ -20,8 +20,8 @@ use nym_credentials_interface::TicketType;
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_gateway_requests::registration::handshake::client_handshake;
|
||||
use nym_gateway_requests::{
|
||||
BinaryRequest, ClientControlRequest, ClientRequest, SensitiveServerResponse, ServerResponse,
|
||||
SharedGatewayKey, SharedSymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION,
|
||||
BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersionExt,
|
||||
SensitiveServerResponse, ServerResponse, SharedGatewayKey, SharedSymmetricKey,
|
||||
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION,
|
||||
};
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
@@ -563,28 +563,10 @@ impl<C, St> GatewayClient<C, St> {
|
||||
Ok(zeroizing_updated_key)
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
debug!("authenticating with gateway");
|
||||
|
||||
let self_address = self
|
||||
.local_identity
|
||||
.as_ref()
|
||||
.public_key()
|
||||
.derive_destination_address();
|
||||
|
||||
let msg = ClientControlRequest::new_authenticate(
|
||||
self_address,
|
||||
shared_key,
|
||||
self.cfg.bandwidth.require_tickets,
|
||||
)?;
|
||||
|
||||
async fn send_authenticate_request_and_handle_response(
|
||||
&mut self,
|
||||
msg: ClientControlRequest,
|
||||
) -> Result<(), GatewayClientError> {
|
||||
match self.send_websocket_message(msg).await? {
|
||||
ServerResponse::Authenticate {
|
||||
protocol_version,
|
||||
@@ -608,6 +590,51 @@ impl<C, St> GatewayClient<C, St> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn authenticate_v1(&mut self) -> Result<(), GatewayClientError> {
|
||||
debug!("using v1 authentication");
|
||||
|
||||
let Some(shared_key) = self.shared_key.as_ref() else {
|
||||
return Err(GatewayClientError::NoSharedKeyAvailable);
|
||||
};
|
||||
|
||||
let self_address = self
|
||||
.local_identity
|
||||
.public_key()
|
||||
.derive_destination_address();
|
||||
|
||||
let msg = ClientControlRequest::new_authenticate(
|
||||
self_address,
|
||||
shared_key,
|
||||
self.cfg.bandwidth.require_tickets,
|
||||
)?;
|
||||
self.send_authenticate_request_and_handle_response(msg)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn authenticate_v2(&mut self) -> Result<(), GatewayClientError> {
|
||||
debug!("using v2 authentication");
|
||||
let Some(shared_key) = self.shared_key.as_ref() else {
|
||||
return Err(GatewayClientError::NoSharedKeyAvailable);
|
||||
};
|
||||
|
||||
let msg = ClientControlRequest::new_authenticate_v2(shared_key, &self.local_identity)?;
|
||||
self.send_authenticate_request_and_handle_response(msg)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn authenticate(&mut self, use_v2: bool) -> Result<(), GatewayClientError> {
|
||||
if !self.connection.is_established() {
|
||||
return Err(GatewayClientError::ConnectionNotEstablished);
|
||||
}
|
||||
debug!("authenticating with gateway");
|
||||
|
||||
if use_v2 {
|
||||
self.authenticate_v2().await
|
||||
} else {
|
||||
self.authenticate_v1().await
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper method to either call register or authenticate based on self.shared_key value
|
||||
#[instrument(skip_all,
|
||||
fields(
|
||||
@@ -623,19 +650,25 @@ impl<C, St> GatewayClient<C, St> {
|
||||
}
|
||||
|
||||
// 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,
|
||||
let gw_protocol = match self.get_gateway_protocol().await {
|
||||
Ok(protocol) => Some(protocol),
|
||||
Err(_) => {
|
||||
// if we failed to send the request, it means the gateway is running the old binary,
|
||||
// so it has reset our connection - we have to reconnect
|
||||
self.establish_connection().await?;
|
||||
false
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let supports_aes_gcm_siv = gw_protocol.supports_aes256_gcm_siv();
|
||||
let supports_auth_v2 = gw_protocol.supports_authenticate_v2();
|
||||
|
||||
if !supports_aes_gcm_siv {
|
||||
warn!("this gateway is on an old version that doesn't support AES256-GCM-SIV");
|
||||
}
|
||||
if !supports_auth_v2 {
|
||||
warn!("this gateway is on an old version that doesn't support authentication v2")
|
||||
}
|
||||
|
||||
if self.authenticated {
|
||||
debug!("Already authenticated");
|
||||
@@ -650,7 +683,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
}
|
||||
|
||||
if self.shared_key.is_some() {
|
||||
self.authenticate().await?;
|
||||
self.authenticate(supports_auth_v2).await?;
|
||||
|
||||
if self.authenticated {
|
||||
// if we are authenticated it means we MUST have an associated shared_key
|
||||
@@ -983,7 +1016,8 @@ impl<C, St> GatewayClient<C, St> {
|
||||
}
|
||||
|
||||
// if we're reconnecting, because we lost connection, we need to re-authenticate the connection
|
||||
self.authenticate().await?;
|
||||
self.authenticate(self.negotiated_protocol.supports_authenticate_v2())
|
||||
.await?;
|
||||
|
||||
// this call is NON-blocking
|
||||
self.start_listening_for_mixnet_messages()?;
|
||||
|
||||
@@ -20,11 +20,14 @@ serde_json = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true, features = ["log"] }
|
||||
time = { workspace = true }
|
||||
subtle = { workspace = true }
|
||||
zeroize = { workspace = true }
|
||||
|
||||
nym-crypto = { path = "../crypto", features = ["aead", "hashing"] }
|
||||
nym-pemstore = { path = "../pemstore" }
|
||||
nym-sphinx = { path = "../nymsphinx" }
|
||||
nym-serde-helpers = { path = "../serde-helpers", features = ["base64"] }
|
||||
nym-task = { path = "../task" }
|
||||
|
||||
nym-credentials = { path = "../credentials" }
|
||||
|
||||
@@ -15,6 +15,12 @@ use thiserror::Error;
|
||||
// this is no longer constant size due to the differences in ciphertext between aes128ctr and aes256gcm-siv (inclusion of tag)
|
||||
pub struct EncryptedAddressBytes(Vec<u8>);
|
||||
|
||||
impl From<Vec<u8>> for EncryptedAddressBytes {
|
||||
fn from(encrypted_address: Vec<u8>) -> Self {
|
||||
EncryptedAddressBytes(encrypted_address)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum EncryptedAddressConversionError {
|
||||
#[error("Failed to decode the encrypted address - {0}")]
|
||||
|
||||
@@ -19,7 +19,7 @@ pub use shared_key::{
|
||||
SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey,
|
||||
};
|
||||
|
||||
pub const CURRENT_PROTOCOL_VERSION: u8 = AES_GCM_SIV_PROTOCOL_VERSION;
|
||||
pub const CURRENT_PROTOCOL_VERSION: u8 = AUTHENTICATE_V2_PROTOCOL_VERSION;
|
||||
|
||||
/// Defines the current version of the communication protocol between gateway and clients.
|
||||
/// It has to be incremented for any breaking change.
|
||||
@@ -27,10 +27,29 @@ pub const CURRENT_PROTOCOL_VERSION: u8 = AES_GCM_SIV_PROTOCOL_VERSION;
|
||||
// 1 - initial release
|
||||
// 2 - changes to client credentials structure
|
||||
// 3 - change to AES-GCM-SIV and non-zero IVs
|
||||
// 4 - introduction of v2 authentication protocol to prevent reply attacks
|
||||
pub const INITIAL_PROTOCOL_VERSION: u8 = 1;
|
||||
pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: u8 = 2;
|
||||
pub const AES_GCM_SIV_PROTOCOL_VERSION: u8 = 3;
|
||||
pub const AUTHENTICATE_V2_PROTOCOL_VERSION: u8 = 4;
|
||||
|
||||
// TODO: could using `Mac` trait here for OutputSize backfire?
|
||||
// Should hmac itself be exposed, imported and used instead?
|
||||
pub type LegacyGatewayMacSize = <GatewayIntegrityHmacAlgorithm as OutputSizeUser>::OutputSize;
|
||||
|
||||
pub trait GatewayProtocolVersionExt {
|
||||
fn supports_aes256_gcm_siv(&self) -> bool;
|
||||
fn supports_authenticate_v2(&self) -> bool;
|
||||
}
|
||||
|
||||
impl GatewayProtocolVersionExt for Option<u8> {
|
||||
fn supports_aes256_gcm_siv(&self) -> bool {
|
||||
let Some(protocol) = *self else { return false };
|
||||
protocol >= AES_GCM_SIV_PROTOCOL_VERSION
|
||||
}
|
||||
|
||||
fn supports_authenticate_v2(&self) -> bool {
|
||||
let Some(protocol) = *self else { return false };
|
||||
protocol >= AUTHENTICATE_V2_PROTOCOL_VERSION
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use crate::SharedKeyUsageError;
|
||||
use nym_credentials_interface::CompactEcashError;
|
||||
use nym_crypto::asymmetric::ed25519::SignatureError;
|
||||
use nym_sphinx::addressing::nodes::NymNodeRoutingAddressError;
|
||||
use nym_sphinx::forwarding::packet::MixPacketFormattingError;
|
||||
use nym_sphinx::params::packet_sizes::PacketSize;
|
||||
@@ -92,7 +93,34 @@ pub enum GatewayRequestsError {
|
||||
#[error("the provided [v1] credential has invalid number of parameters - {0}")]
|
||||
InvalidNumberOfEmbededParameters(u32),
|
||||
|
||||
#[error("failed to authenticate the client: {0}")]
|
||||
Authentication(#[from] AuthenticationFailure),
|
||||
|
||||
// variant to catch legacy errors
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AuthenticationFailure {
|
||||
#[error(transparent)]
|
||||
KeyUsageFailure(#[from] SharedKeyUsageError),
|
||||
|
||||
#[error("failed to verify provided address ciphertext")]
|
||||
MalformedCiphertext,
|
||||
|
||||
#[error("failed to verify request signature")]
|
||||
InvalidSignature(#[from] SignatureError),
|
||||
|
||||
#[error("provided request timestamp is in the future")]
|
||||
RequestTimestampInFuture,
|
||||
|
||||
#[error("the client is not registered")]
|
||||
NotRegistered,
|
||||
|
||||
#[error("the provided request is too stale to process")]
|
||||
StaleRequest,
|
||||
|
||||
#[error("the provided request timestamp is smaller or equal to a one previously used")]
|
||||
RequestReuse,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::{AuthenticationFailure, GatewayRequestsError, SharedGatewayKey};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::iter;
|
||||
use std::time::Duration;
|
||||
use subtle::ConstantTimeEq;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthenticateRequest {
|
||||
#[serde(flatten)]
|
||||
pub content: AuthenticateRequestContent,
|
||||
|
||||
pub request_signature: ed25519::Signature,
|
||||
}
|
||||
|
||||
impl AuthenticateRequest {
|
||||
pub fn new(
|
||||
protocol_version: u8,
|
||||
shared_key: &SharedGatewayKey,
|
||||
identity_keys: &ed25519::KeyPair,
|
||||
) -> Result<AuthenticateRequest, GatewayRequestsError> {
|
||||
let content = AuthenticateRequestContent::new(
|
||||
protocol_version,
|
||||
shared_key,
|
||||
*identity_keys.public_key(),
|
||||
)?;
|
||||
let plaintext = content.plaintext();
|
||||
let request_signature = identity_keys.private_key().sign(&plaintext);
|
||||
|
||||
Ok(AuthenticateRequest {
|
||||
content,
|
||||
request_signature,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn verify_timestamp(&self, max_request_age: Duration) -> Result<(), AuthenticationFailure> {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
if self.content.request_timestamp() + max_request_age < now {
|
||||
return Err(AuthenticationFailure::StaleRequest);
|
||||
}
|
||||
if self.content.request_timestamp() > now {
|
||||
return Err(AuthenticationFailure::RequestTimestampInFuture);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ensure_timestamp_not_reused(
|
||||
&self,
|
||||
previous: OffsetDateTime,
|
||||
) -> Result<(), AuthenticationFailure> {
|
||||
if self.content.request_timestamp() <= previous {
|
||||
return Err(AuthenticationFailure::RequestReuse);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn verify_ciphertext(
|
||||
&self,
|
||||
shared_key: &SharedGatewayKey,
|
||||
) -> Result<(), AuthenticationFailure> {
|
||||
let expected = shared_key.encrypt(
|
||||
self.content
|
||||
.client_identity
|
||||
.derive_destination_address()
|
||||
.as_bytes_ref(),
|
||||
Some(&self.content.nonce),
|
||||
)?;
|
||||
|
||||
if !bool::from(expected.ct_eq(&self.content.address_ciphertext)) {
|
||||
return Err(AuthenticationFailure::MalformedCiphertext);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn verify_signature(&self) -> Result<(), AuthenticationFailure> {
|
||||
let plaintext = self.content.plaintext();
|
||||
self.content
|
||||
.client_identity
|
||||
.verify(plaintext, &self.request_signature)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthenticateRequestContent {
|
||||
pub protocol_version: u8,
|
||||
|
||||
// this is identical to the client's address
|
||||
pub client_identity: ed25519::PublicKey,
|
||||
|
||||
#[serde(with = "nym_serde_helpers::base64")]
|
||||
pub address_ciphertext: Vec<u8>,
|
||||
|
||||
#[serde(with = "nym_serde_helpers::base64")]
|
||||
pub nonce: Vec<u8>,
|
||||
|
||||
pub request_unix_timestamp: u64,
|
||||
}
|
||||
|
||||
impl AuthenticateRequestContent {
|
||||
fn new(
|
||||
protocol_version: u8,
|
||||
shared_key: &SharedGatewayKey,
|
||||
client_identity: ed25519::PublicKey,
|
||||
) -> Result<AuthenticateRequestContent, GatewayRequestsError> {
|
||||
let nonce = shared_key.random_nonce_or_iv();
|
||||
let destination_address = client_identity.derive_destination_address();
|
||||
|
||||
let address_ciphertext =
|
||||
shared_key.encrypt(destination_address.as_bytes_ref(), Some(&nonce))?;
|
||||
let now = OffsetDateTime::now_utc();
|
||||
Ok(AuthenticateRequestContent {
|
||||
protocol_version,
|
||||
client_identity,
|
||||
address_ciphertext,
|
||||
nonce,
|
||||
request_unix_timestamp: now.unix_timestamp() as u64, // SAFETY: we're running this in post 1970...
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthenticateRequestContent {
|
||||
pub fn plaintext(&self) -> Vec<u8> {
|
||||
iter::once(self.protocol_version)
|
||||
.chain(self.client_identity.to_bytes())
|
||||
.chain(self.address_ciphertext.iter().copied())
|
||||
.chain(self.nonce.iter().copied())
|
||||
.chain(self.request_unix_timestamp.to_be_bytes())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn request_timestamp(&self) -> OffsetDateTime {
|
||||
OffsetDateTime::from_unix_timestamp(self.request_unix_timestamp as i64)
|
||||
.unwrap_or(OffsetDateTime::UNIX_EPOCH)
|
||||
}
|
||||
}
|
||||
+22
-1
@@ -2,16 +2,21 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::models::CredentialSpendingRequest;
|
||||
use crate::text_request::authenticate::AuthenticateRequest;
|
||||
use crate::{
|
||||
GatewayRequestsError, SharedGatewayKey, SymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION,
|
||||
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION,
|
||||
AUTHENTICATE_V2_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION,
|
||||
INITIAL_PROTOCOL_VERSION,
|
||||
};
|
||||
use nym_credentials_interface::CredentialSpendingData;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_sphinx::DestinationAddressBytes;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
use tungstenite::Message;
|
||||
|
||||
pub mod authenticate;
|
||||
|
||||
// wrapper for all encrypted requests for ease of use
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
@@ -68,6 +73,9 @@ pub enum ClientControlRequest {
|
||||
enc_address: String,
|
||||
iv: String,
|
||||
},
|
||||
|
||||
AuthenticateV2(Box<AuthenticateRequest>),
|
||||
|
||||
#[serde(alias = "handshakePayload")]
|
||||
RegisterHandshakeInitRequest {
|
||||
#[serde(default)]
|
||||
@@ -123,9 +131,22 @@ impl ClientControlRequest {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_authenticate_v2(
|
||||
shared_key: &SharedGatewayKey,
|
||||
identity_keys: &ed25519::KeyPair,
|
||||
) -> Result<Self, GatewayRequestsError> {
|
||||
// if we're using v2 authentication, we must announce at least that protocol version
|
||||
let protocol_version = AUTHENTICATE_V2_PROTOCOL_VERSION;
|
||||
|
||||
Ok(ClientControlRequest::AuthenticateV2(Box::new(
|
||||
AuthenticateRequest::new(protocol_version, shared_key, identity_keys)?,
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
match self {
|
||||
ClientControlRequest::Authenticate { .. } => "Authenticate".to_string(),
|
||||
ClientControlRequest::AuthenticateV2(..) => "AuthenticateV2".to_string(),
|
||||
ClientControlRequest::RegisterHandshakeInitRequest { .. } => {
|
||||
"RegisterHandshakeInitRequest".to_string()
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/*
|
||||
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
ALTER TABLE shared_keys
|
||||
ADD COLUMN last_used_authentication TIMESTAMP WITHOUT TIME ZONE;
|
||||
@@ -200,6 +200,20 @@ impl GatewayStorage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_last_used_authentication_timestamp(
|
||||
&self,
|
||||
client_id: i64,
|
||||
last_used_authentication_timestamp: OffsetDateTime,
|
||||
) -> Result<(), GatewayStorageError> {
|
||||
self.shared_key_manager
|
||||
.update_last_used_authentication_timestamp(
|
||||
client_id,
|
||||
last_used_authentication_timestamp,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_client(&self, client_id: i64) -> Result<Option<Client>, GatewayStorageError> {
|
||||
let client = self.client_manager.get_client(client_id).await?;
|
||||
Ok(client)
|
||||
|
||||
@@ -14,13 +14,13 @@ pub struct Client {
|
||||
|
||||
#[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: Option<String>,
|
||||
pub derived_aes256_gcm_siv_key: Option<Vec<u8>>,
|
||||
pub last_used_authentication: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
impl TryFrom<PersistedSharedKeys> for SharedGatewayKey {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::models::PersistedSharedKeys;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct SharedKeysManager {
|
||||
@@ -68,6 +69,22 @@ impl SharedKeysManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn update_last_used_authentication_timestamp(
|
||||
&self,
|
||||
client_id: i64,
|
||||
last_used: OffsetDateTime,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
"UPDATE shared_keys SET last_used_authentication = ? WHERE client_id = ?;",
|
||||
last_used,
|
||||
client_id
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tries to retrieve shared keys stored for the particular client.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -77,13 +94,10 @@ impl SharedKeysManager {
|
||||
&self,
|
||||
client_address_bs58: &str,
|
||||
) -> Result<Option<PersistedSharedKeys>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
PersistedSharedKeys,
|
||||
"SELECT * FROM shared_keys WHERE client_address_bs58 = ?",
|
||||
client_address_bs58
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await
|
||||
sqlx::query_as("SELECT * FROM shared_keys WHERE client_address_bs58 = ?")
|
||||
.bind(client_address_bs58)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Removes from the database shared keys derived with the particular client.
|
||||
|
||||
@@ -98,6 +98,9 @@ pub struct Debug {
|
||||
pub stale_messages_max_age: Duration,
|
||||
|
||||
pub zk_nym_tickets: ZkNymTicketHandlerDebug,
|
||||
|
||||
/// Defines the maximum age of a signed authentication request before it's deemed too stale to process.
|
||||
pub maximum_auth_request_age: Duration,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -6,11 +6,20 @@ use crate::node::client_handling::embedded_clients::LocalEmbeddedClientHandle;
|
||||
use dashmap::DashMap;
|
||||
use nym_sphinx::DestinationAddressBytes;
|
||||
use std::sync::Arc;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::warn;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RemoteClientData {
|
||||
// note, this does **NOT** indicate timestamp of when client connected
|
||||
// it is (for v2 auth) timestamp the client **signed** when it created the request
|
||||
pub(crate) session_request_timestamp: OffsetDateTime,
|
||||
pub(crate) channels: ClientIncomingChannels,
|
||||
}
|
||||
|
||||
enum ActiveClient {
|
||||
/// Handle to a remote client connected via a network socket.
|
||||
Remote(ClientIncomingChannels),
|
||||
Remote(RemoteClientData),
|
||||
|
||||
/// Handle to a locally (inside the same process) running client.
|
||||
Embedded(LocalEmbeddedClientHandle),
|
||||
@@ -19,14 +28,14 @@ enum ActiveClient {
|
||||
impl ActiveClient {
|
||||
fn get_sender_ref(&self) -> &MixMessageSender {
|
||||
match self {
|
||||
ActiveClient::Remote(remote) => &remote.mix_message_sender,
|
||||
ActiveClient::Remote(remote) => &remote.channels.mix_message_sender,
|
||||
ActiveClient::Embedded(embedded) => &embedded.mix_message_sender,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_sender(&self) -> MixMessageSender {
|
||||
match self {
|
||||
ActiveClient::Remote(remote) => remote.mix_message_sender.clone(),
|
||||
ActiveClient::Remote(remote) => remote.channels.mix_message_sender.clone(),
|
||||
ActiveClient::Embedded(embedded) => embedded.mix_message_sender.clone(),
|
||||
}
|
||||
}
|
||||
@@ -78,18 +87,18 @@ impl ActiveClientsStore {
|
||||
pub(crate) fn get_remote_client(
|
||||
&self,
|
||||
address: DestinationAddressBytes,
|
||||
) -> Option<ClientIncomingChannels> {
|
||||
) -> Option<RemoteClientData> {
|
||||
let entry = self.inner.get(&address)?;
|
||||
let handle = entry.value();
|
||||
|
||||
let ActiveClient::Remote(channels) = handle else {
|
||||
let ActiveClient::Remote(remote) = handle else {
|
||||
warn!("attempted to get a remote handle to a embedded network requester");
|
||||
return None;
|
||||
};
|
||||
|
||||
// if the entry is stale, remove it from the map
|
||||
if !channels.mix_message_sender.is_closed() {
|
||||
Some(channels.clone())
|
||||
if !remote.channels.mix_message_sender.is_closed() {
|
||||
Some(remote.clone())
|
||||
} else {
|
||||
// drop the reference to the map to prevent deadlocks
|
||||
drop(entry);
|
||||
@@ -137,10 +146,14 @@ impl ActiveClientsStore {
|
||||
client: DestinationAddressBytes,
|
||||
handle: MixMessageSender,
|
||||
is_active_request_sender: IsActiveRequestSender,
|
||||
session_request_timestamp: OffsetDateTime,
|
||||
) {
|
||||
let entry = ActiveClient::Remote(ClientIncomingChannels {
|
||||
mix_message_sender: handle,
|
||||
is_active_request_sender,
|
||||
let entry = ActiveClient::Remote(RemoteClientData {
|
||||
session_request_timestamp,
|
||||
channels: ClientIncomingChannels {
|
||||
mix_message_sender: handle,
|
||||
is_active_request_sender,
|
||||
},
|
||||
});
|
||||
if self.inner.insert(client, entry).is_some() {
|
||||
panic!("inserted a duplicate remote client")
|
||||
|
||||
@@ -9,15 +9,23 @@ use nym_mixnet_client::forwarder::MixForwardingSender;
|
||||
use nym_node_metrics::events::MetricEventsSender;
|
||||
use nym_node_metrics::NymNodeMetrics;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Config {
|
||||
pub(crate) enforce_zk_nym: bool,
|
||||
pub(crate) max_auth_request_age: Duration,
|
||||
|
||||
pub(crate) bandwidth: BandwidthFlushingBehaviourConfig,
|
||||
}
|
||||
|
||||
// I can see this being possible expanded with say storage or client store
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CommonHandlerState {
|
||||
pub(crate) cfg: Config,
|
||||
pub(crate) ecash_verifier: Arc<EcashManager>,
|
||||
pub(crate) storage: GatewayStorage,
|
||||
pub(crate) local_identity: Arc<identity::KeyPair>,
|
||||
pub(crate) only_coconut_credentials: bool,
|
||||
pub(crate) bandwidth_cfg: BandwidthFlushingBehaviourConfig,
|
||||
pub(crate) metrics: NymNodeMetrics,
|
||||
pub(crate) metrics_sender: MetricEventsSender,
|
||||
pub(crate) outbound_mix_sender: MixForwardingSender,
|
||||
|
||||
@@ -194,8 +194,8 @@ impl<R, S> AuthenticatedHandler<R, S> {
|
||||
fresh.shared_state.storage.clone(),
|
||||
ClientBandwidth::new(bandwidth.into()),
|
||||
client.id,
|
||||
fresh.shared_state.bandwidth_cfg,
|
||||
fresh.shared_state.only_coconut_credentials,
|
||||
fresh.shared_state.cfg.bandwidth,
|
||||
fresh.shared_state.cfg.enforce_zk_nym,
|
||||
),
|
||||
inner: fresh,
|
||||
client,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::client_handling::active_clients::RemoteClientData;
|
||||
use crate::node::client_handling::websocket::common_state::CommonHandlerState;
|
||||
use crate::node::client_handling::websocket::connection_handler::helpers::KeyWithAuthTimestamp;
|
||||
use crate::node::client_handling::websocket::connection_handler::INITIAL_MESSAGE_TIMEOUT;
|
||||
use crate::node::client_handling::websocket::{
|
||||
connection_handler::{AuthenticatedHandler, ClientDetails, InitialAuthResult, SocketStream},
|
||||
message_receiver::{IsActive, IsActiveRequestSender},
|
||||
message_receiver::IsActive,
|
||||
};
|
||||
use futures::{
|
||||
channel::{mpsc, oneshot},
|
||||
@@ -13,14 +15,16 @@ use futures::{
|
||||
};
|
||||
use nym_credentials_interface::AvailableBandwidth;
|
||||
use nym_crypto::aes::cipher::crypto_common::rand_core::RngCore;
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_gateway_requests::authenticate::AuthenticateRequest;
|
||||
use nym_gateway_requests::authentication::encrypted_address::{
|
||||
EncryptedAddressBytes, EncryptedAddressConversionError,
|
||||
};
|
||||
use nym_gateway_requests::{
|
||||
registration::handshake::{error::HandshakeError, gateway_handshake},
|
||||
types::{ClientControlRequest, ServerResponse},
|
||||
BinaryResponse, SharedGatewayKey, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION,
|
||||
AuthenticationFailure, BinaryResponse, SharedGatewayKey, CURRENT_PROTOCOL_VERSION,
|
||||
INITIAL_PROTOCOL_VERSION,
|
||||
};
|
||||
use nym_gateway_storage::error::GatewayStorageError;
|
||||
use nym_node_metrics::events::MetricsEvent;
|
||||
@@ -30,6 +34,7 @@ use rand::CryptoRng;
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio::time::timeout;
|
||||
use tokio_tungstenite::tungstenite::{protocol::Message, Error as WsError};
|
||||
@@ -37,6 +42,12 @@ use tracing::*;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub(crate) enum InitialAuthenticationError {
|
||||
#[error(transparent)]
|
||||
AuthenticationFailure(#[from] AuthenticationFailure),
|
||||
|
||||
#[error("attempted to overwrite client session with a stale authentication")]
|
||||
StaleSessionOverwrite,
|
||||
|
||||
#[error("Internal gateway storage error")]
|
||||
StorageError(#[from] GatewayStorageError),
|
||||
|
||||
@@ -290,15 +301,15 @@ impl<R, S> FreshHandler<R, S> {
|
||||
// of doing full parse of the init_data elsewhere
|
||||
fn extract_remote_identity_from_register_init(
|
||||
init_data: &[u8],
|
||||
) -> Result<identity::PublicKey, InitialAuthenticationError> {
|
||||
if init_data.len() < identity::PUBLIC_KEY_LENGTH {
|
||||
) -> Result<ed25519::PublicKey, InitialAuthenticationError> {
|
||||
if init_data.len() < ed25519::PUBLIC_KEY_LENGTH {
|
||||
Err(InitialAuthenticationError::HandshakeError(
|
||||
HandshakeError::MalformedRequest,
|
||||
))
|
||||
} else {
|
||||
identity::PublicKey::from_bytes(&init_data[..identity::PUBLIC_KEY_LENGTH]).map_err(
|
||||
|_| InitialAuthenticationError::HandshakeError(HandshakeError::MalformedRequest),
|
||||
)
|
||||
ed25519::PublicKey::from_bytes(&init_data[..ed25519::PUBLIC_KEY_LENGTH]).map_err(|_| {
|
||||
InitialAuthenticationError::HandshakeError(HandshakeError::MalformedRequest)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,6 +362,21 @@ impl<R, S> FreshHandler<R, S> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn retrieve_shared_key(
|
||||
&self,
|
||||
client: DestinationAddressBytes,
|
||||
) -> Result<Option<KeyWithAuthTimestamp>, InitialAuthenticationError> {
|
||||
let shared_keys = self.shared_state.storage.get_shared_keys(client).await?;
|
||||
|
||||
let Some(stored_shared_keys) = shared_keys else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let keys = KeyWithAuthTimestamp::try_from_stored(stored_shared_keys, client)?;
|
||||
|
||||
Ok(Some(keys))
|
||||
}
|
||||
|
||||
/// Checks whether the stored shared keys match the received data, i.e. whether the upon decryption
|
||||
/// the provided encrypted address matches the expected unencrypted address.
|
||||
///
|
||||
@@ -361,31 +387,18 @@ impl<R, S> FreshHandler<R, S> {
|
||||
/// * `client_address`: address of the client.
|
||||
/// * `encrypted_address`: encrypted address of the client, presumably encrypted using the shared keys.
|
||||
/// * `iv`: nonce/iv created for this particular encryption.
|
||||
async fn verify_stored_shared_key(
|
||||
async fn auth_v1_verify_stored_shared_key(
|
||||
&self,
|
||||
client_address: DestinationAddressBytes,
|
||||
encrypted_address: EncryptedAddressBytes,
|
||||
nonce: &[u8],
|
||||
) -> Result<Option<SharedGatewayKey>, InitialAuthenticationError> {
|
||||
let shared_keys = self
|
||||
.shared_state
|
||||
.storage
|
||||
.get_shared_keys(client_address)
|
||||
.await?;
|
||||
|
||||
let Some(stored_shared_keys) = shared_keys else {
|
||||
) -> Result<Option<KeyWithAuthTimestamp>, InitialAuthenticationError> {
|
||||
let Some(keys) = self.retrieve_shared_key(client_address).await? 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) {
|
||||
if encrypted_address.verify(&client_address, &keys.key, nonce) {
|
||||
Ok(Some(keys))
|
||||
} else {
|
||||
Ok(None)
|
||||
@@ -428,49 +441,19 @@ impl<R, S> FreshHandler<R, S> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Using the received challenge data, i.e. client's address as well the ciphertext of it plus
|
||||
/// a fresh IV, attempts to authenticate the client by checking whether the ciphertext matches
|
||||
/// the expected value if encrypted with the shared key.
|
||||
///
|
||||
/// Finally, upon completion, all previously stored messages are pushed back to the client.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `client_address`: address of the client wishing to authenticate.
|
||||
/// * `encrypted_address`: ciphertext of the address of the client wishing to authenticate.
|
||||
/// * `iv`: fresh nonce/IV received with the request.
|
||||
async fn authenticate_client(
|
||||
&mut self,
|
||||
client_address: DestinationAddressBytes,
|
||||
encrypted_address: EncryptedAddressBytes,
|
||||
nonce: &[u8],
|
||||
) -> Result<Option<SharedGatewayKey>, InitialAuthenticationError>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
debug!(
|
||||
"Processing authenticate client request for: {}",
|
||||
client_address.as_base58_string()
|
||||
);
|
||||
|
||||
let shared_keys = self
|
||||
.verify_stored_shared_key(client_address, encrypted_address, nonce)
|
||||
.await?;
|
||||
|
||||
if let Some(shared_keys) = shared_keys {
|
||||
self.push_stored_messages_to_client(client_address, &shared_keys)
|
||||
.await?;
|
||||
Ok(Some(shared_keys))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_duplicate_client(
|
||||
&mut self,
|
||||
address: DestinationAddressBytes,
|
||||
mut is_active_request_tx: IsActiveRequestSender,
|
||||
remote_client_data: RemoteClientData,
|
||||
new_session_start: OffsetDateTime,
|
||||
) -> Result<(), InitialAuthenticationError> {
|
||||
let mut is_active_request_tx = remote_client_data.channels.is_active_request_sender;
|
||||
|
||||
// new session must **always** be explicitly more recent
|
||||
if new_session_start <= remote_client_data.session_request_timestamp {
|
||||
return Err(InitialAuthenticationError::StaleSessionOverwrite);
|
||||
}
|
||||
|
||||
// Ask the other connection to ping if they are still active.
|
||||
// Use a oneshot channel to return the result to us
|
||||
let (ping_result_sender, ping_result_receiver) = oneshot::channel();
|
||||
@@ -519,6 +502,32 @@ impl<R, S> FreshHandler<R, S> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn get_registered_client_id(
|
||||
&self,
|
||||
client_address: DestinationAddressBytes,
|
||||
) -> Result<i64, InitialAuthenticationError> {
|
||||
self.shared_state
|
||||
.storage
|
||||
.get_mixnet_client_id(client_address)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_registered_available_bandwidth(
|
||||
&self,
|
||||
client_id: i64,
|
||||
) -> Result<AvailableBandwidth, InitialAuthenticationError> {
|
||||
let available_bandwidth: AvailableBandwidth = self
|
||||
.shared_state
|
||||
.storage
|
||||
.get_available_bandwidth(client_id)
|
||||
.await?
|
||||
.map(From::from)
|
||||
.unwrap_or_default();
|
||||
Ok(available_bandwidth)
|
||||
}
|
||||
|
||||
/// Tries to handle the received authentication request by checking correctness of the received data.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -531,7 +540,7 @@ impl<R, S> FreshHandler<R, S> {
|
||||
address = %address,
|
||||
)
|
||||
)]
|
||||
async fn handle_authenticate(
|
||||
async fn handle_legacy_authenticate(
|
||||
&mut self,
|
||||
client_protocol_version: Option<u8>,
|
||||
address: String,
|
||||
@@ -541,7 +550,7 @@ impl<R, S> FreshHandler<R, S> {
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
debug!("handling client registration");
|
||||
debug!("handling client authentication (v1)");
|
||||
|
||||
let negotiated_protocol = self.negotiate_client_protocol(client_protocol_version)?;
|
||||
// populate the negotiated protocol for future uses
|
||||
@@ -554,38 +563,38 @@ impl<R, S> FreshHandler<R, S> {
|
||||
.into_vec()
|
||||
.map_err(InitialAuthenticationError::MalformedIV)?;
|
||||
|
||||
// Check for duplicate clients
|
||||
if let Some(client_tx) = self
|
||||
.shared_state
|
||||
.active_clients_store
|
||||
.get_remote_client(address)
|
||||
{
|
||||
warn!("Detected duplicate connection for client: {address}");
|
||||
self.handle_duplicate_client(address, client_tx.is_active_request_sender)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// validate the shared key
|
||||
let Some(shared_keys) = self
|
||||
.authenticate_client(address, encrypted_address, &nonce)
|
||||
.auth_v1_verify_stored_shared_key(address, encrypted_address, &nonce)
|
||||
.await?
|
||||
else {
|
||||
// it feels weird to be returning an 'Ok' here, but I didn't want to change the existing behaviour
|
||||
return Ok(InitialAuthResult::new_failed(Some(negotiated_protocol)));
|
||||
};
|
||||
|
||||
let client_id = self
|
||||
// in v1 we don't have explicit data so we have to use current timestamp
|
||||
// (which does nothing but just allows us to use the same codepath)
|
||||
let session_request_start = OffsetDateTime::now_utc();
|
||||
|
||||
// Check for duplicate clients
|
||||
if let Some(remote_client_data) = self
|
||||
.shared_state
|
||||
.storage
|
||||
.get_mixnet_client_id(address)
|
||||
.active_clients_store
|
||||
.get_remote_client(address)
|
||||
{
|
||||
warn!("Detected duplicate connection for client: {address}");
|
||||
self.handle_duplicate_client(address, remote_client_data, session_request_start)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let client_id = shared_keys.client_id;
|
||||
|
||||
// if applicable, push stored messages
|
||||
self.push_stored_messages_to_client(address, &shared_keys.key)
|
||||
.await?;
|
||||
|
||||
let available_bandwidth: AvailableBandwidth = self
|
||||
.shared_state
|
||||
.storage
|
||||
.get_available_bandwidth(client_id)
|
||||
.await?
|
||||
.map(From::from)
|
||||
.unwrap_or_default();
|
||||
// check the bandwidth
|
||||
let available_bandwidth = self.get_registered_available_bandwidth(client_id).await?;
|
||||
|
||||
let bandwidth_remaining = if available_bandwidth.expired() {
|
||||
self.shared_state.storage.reset_bandwidth(client_id).await?;
|
||||
@@ -595,7 +604,98 @@ impl<R, S> FreshHandler<R, S> {
|
||||
};
|
||||
|
||||
Ok(InitialAuthResult::new(
|
||||
Some(ClientDetails::new(client_id, address, shared_keys)),
|
||||
Some(ClientDetails::new(
|
||||
client_id,
|
||||
address,
|
||||
shared_keys.key,
|
||||
session_request_start,
|
||||
)),
|
||||
ServerResponse::Authenticate {
|
||||
protocol_version: Some(negotiated_protocol),
|
||||
status: true,
|
||||
bandwidth_remaining,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
async fn handle_authenticate_v2(
|
||||
&mut self,
|
||||
request: Box<AuthenticateRequest>,
|
||||
) -> Result<InitialAuthResult, InitialAuthenticationError>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
debug!("handling client authentication (v2)");
|
||||
|
||||
let negotiated_protocol =
|
||||
self.negotiate_client_protocol(Some(request.content.protocol_version))?;
|
||||
// populate the negotiated protocol for future uses
|
||||
self.negotiated_protocol = Some(negotiated_protocol);
|
||||
|
||||
let address = request.content.client_identity.derive_destination_address();
|
||||
|
||||
// do cheap checks first
|
||||
// is the provided timestamp relatively recent (and not in the future?)
|
||||
request.verify_timestamp(self.shared_state.cfg.max_auth_request_age)?;
|
||||
|
||||
// does the message signature verify?
|
||||
request.verify_signature()?;
|
||||
|
||||
// retrieve the actually stored key and check if the ciphertext matches
|
||||
let Some(shared_key) = self.retrieve_shared_key(address).await? else {
|
||||
return Err(AuthenticationFailure::NotRegistered)?;
|
||||
};
|
||||
request.verify_ciphertext(&shared_key.key)?;
|
||||
|
||||
let session_request_start = request.content.request_timestamp();
|
||||
|
||||
// if the client has already authenticated in the past, make sure this authentication timestamp
|
||||
// is different and greater than the old one (in case it was replayed)
|
||||
if let Some(prior_usage) = shared_key.last_used_authentication {
|
||||
request.ensure_timestamp_not_reused(prior_usage)?;
|
||||
}
|
||||
|
||||
// check for duplicate clients
|
||||
if let Some(client_data) = self
|
||||
.shared_state
|
||||
.active_clients_store
|
||||
.get_remote_client(address)
|
||||
{
|
||||
warn!("Detected duplicate connection for client: {address}");
|
||||
self.handle_duplicate_client(address, client_data, session_request_start)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let client_id = shared_key.client_id;
|
||||
|
||||
// update the auth timestamp for future uses
|
||||
self.shared_state
|
||||
.storage
|
||||
.update_last_used_authentication_timestamp(client_id, session_request_start)
|
||||
.await?;
|
||||
|
||||
// push any old stored messages to the client
|
||||
// (this will be removed soon)
|
||||
self.push_stored_messages_to_client(address, &shared_key.key)
|
||||
.await?;
|
||||
|
||||
// finally check and retrieve client's bandwidth
|
||||
let available_bandwidth = self.get_registered_available_bandwidth(client_id).await?;
|
||||
|
||||
let bandwidth_remaining = if available_bandwidth.expired() {
|
||||
self.shared_state.storage.reset_bandwidth(client_id).await?;
|
||||
0
|
||||
} else {
|
||||
available_bandwidth.bytes
|
||||
};
|
||||
|
||||
Ok(InitialAuthResult::new(
|
||||
Some(ClientDetails::new(
|
||||
client_id,
|
||||
address,
|
||||
shared_key.key,
|
||||
session_request_start,
|
||||
)),
|
||||
ServerResponse::Authenticate {
|
||||
protocol_version: Some(negotiated_protocol),
|
||||
status: true,
|
||||
@@ -688,7 +788,12 @@ impl<R, S> FreshHandler<R, S> {
|
||||
|
||||
debug!(client_id = %client_id, "managed to finalize client registration");
|
||||
|
||||
let client_details = ClientDetails::new(client_id, remote_address, shared_keys);
|
||||
let client_details = ClientDetails::new(
|
||||
client_id,
|
||||
remote_address,
|
||||
shared_keys,
|
||||
OffsetDateTime::now_utc(),
|
||||
);
|
||||
|
||||
Ok(InitialAuthResult::new(
|
||||
Some(client_details),
|
||||
@@ -734,9 +839,10 @@ impl<R, S> FreshHandler<R, S> {
|
||||
enc_address,
|
||||
iv,
|
||||
} => {
|
||||
self.handle_authenticate(protocol_version, address, enc_address, iv)
|
||||
self.handle_legacy_authenticate(protocol_version, address, enc_address, iv)
|
||||
.await
|
||||
}
|
||||
ClientControlRequest::AuthenticateV2(req) => self.handle_authenticate_v2(req).await,
|
||||
ClientControlRequest::RegisterHandshakeInitRequest {
|
||||
protocol_version,
|
||||
data,
|
||||
@@ -827,6 +933,7 @@ impl<R, S> FreshHandler<R, S> {
|
||||
registration_details.address,
|
||||
mix_sender,
|
||||
is_active_request_sender,
|
||||
registration_details.session_request_timestamp,
|
||||
);
|
||||
|
||||
return AuthenticatedHandler::upgrade(
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::client_handling::websocket::connection_handler::fresh::InitialAuthenticationError;
|
||||
use nym_gateway_requests::SharedGatewayKey;
|
||||
use nym_gateway_storage::models::PersistedSharedKeys;
|
||||
use nym_sphinx::DestinationAddressBytes;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
pub(crate) struct KeyWithAuthTimestamp {
|
||||
pub(crate) client_id: i64,
|
||||
pub(crate) key: SharedGatewayKey,
|
||||
pub(crate) last_used_authentication: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
impl KeyWithAuthTimestamp {
|
||||
pub(crate) fn try_from_stored(
|
||||
stored_shared_keys: PersistedSharedKeys,
|
||||
client: DestinationAddressBytes,
|
||||
) -> Result<Self, InitialAuthenticationError> {
|
||||
let last_used_authentication = stored_shared_keys.last_used_authentication;
|
||||
let client_id = stored_shared_keys.client_id;
|
||||
|
||||
let key = SharedGatewayKey::try_from(stored_shared_keys).map_err(|source| {
|
||||
InitialAuthenticationError::MalformedStoredSharedKey {
|
||||
client_id: client.as_base58_string(),
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(KeyWithAuthTimestamp {
|
||||
client_id,
|
||||
key,
|
||||
last_used_authentication,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -8,16 +8,17 @@ use nym_gateway_requests::ServerResponse;
|
||||
use nym_sphinx::DestinationAddressBytes;
|
||||
use rand::{CryptoRng, Rng};
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio_tungstenite::WebSocketStream;
|
||||
use tracing::{debug, instrument, trace, warn};
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
pub(crate) use self::authenticated::AuthenticatedHandler;
|
||||
pub(crate) use self::fresh::FreshHandler;
|
||||
|
||||
pub(crate) mod authenticated;
|
||||
mod fresh;
|
||||
pub(crate) mod helpers;
|
||||
|
||||
const WEBSOCKET_HANDSHAKE_TIMEOUT: Duration = Duration::from_millis(1_500);
|
||||
const INITIAL_MESSAGE_TIMEOUT: Duration = Duration::from_millis(10_000);
|
||||
@@ -40,12 +41,13 @@ impl<S> SocketStream<S> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Zeroize, ZeroizeOnDrop)]
|
||||
pub(crate) struct ClientDetails {
|
||||
#[zeroize(skip)]
|
||||
pub(crate) address: DestinationAddressBytes,
|
||||
pub(crate) id: i64,
|
||||
pub(crate) shared_keys: SharedGatewayKey,
|
||||
// note, this does **NOT ALWAYS** indicate timestamp of when client connected
|
||||
// it is (for v2 auth) timestamp the client **signed** when it created the request
|
||||
pub(crate) session_request_timestamp: OffsetDateTime,
|
||||
}
|
||||
|
||||
impl ClientDetails {
|
||||
@@ -53,11 +55,13 @@ impl ClientDetails {
|
||||
id: i64,
|
||||
address: DestinationAddressBytes,
|
||||
shared_keys: SharedGatewayKey,
|
||||
session_request_timestamp: OffsetDateTime,
|
||||
) -> Self {
|
||||
ClientDetails {
|
||||
address,
|
||||
id,
|
||||
shared_keys,
|
||||
session_request_timestamp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,4 @@ pub(crate) mod connection_handler;
|
||||
pub(crate) mod listener;
|
||||
pub(crate) mod message_receiver;
|
||||
|
||||
pub(crate) use common_state::CommonHandlerState;
|
||||
pub(crate) use common_state::{CommonHandlerState, Config};
|
||||
|
||||
@@ -249,11 +249,14 @@ impl GatewayTasksBuilder {
|
||||
active_clients_store: ActiveClientsStore,
|
||||
) -> Result<websocket::Listener, GatewayError> {
|
||||
let shared_state = websocket::CommonHandlerState {
|
||||
cfg: websocket::Config {
|
||||
enforce_zk_nym: self.config.gateway.enforce_zk_nyms,
|
||||
max_auth_request_age: self.config.debug.maximum_auth_request_age,
|
||||
bandwidth: (&self.config).into(),
|
||||
},
|
||||
ecash_verifier: self.ecash_manager().await?,
|
||||
storage: self.storage.clone(),
|
||||
local_identity: Arc::clone(&self.identity_keypair),
|
||||
only_coconut_credentials: self.config.gateway.enforce_zk_nyms,
|
||||
bandwidth_cfg: (&self.config).into(),
|
||||
metrics: self.metrics.clone(),
|
||||
metrics_sender: self.metrics_sender.clone(),
|
||||
outbound_mix_sender: self.mix_packet_sender.clone(),
|
||||
|
||||
@@ -47,6 +47,9 @@ pub struct Debug {
|
||||
/// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage.
|
||||
pub message_retrieval_limit: i64,
|
||||
|
||||
/// Defines the maximum age of a signed authentication request before it's deemed too stale to process.
|
||||
pub maximum_auth_request_age: Duration,
|
||||
|
||||
pub stale_messages: StaleMessageDebug,
|
||||
|
||||
pub client_bandwidth: ClientBandwidthDebug,
|
||||
@@ -55,13 +58,15 @@ pub struct Debug {
|
||||
}
|
||||
|
||||
impl Debug {
|
||||
const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100;
|
||||
pub const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100;
|
||||
pub const DEFAULT_MAXIMUM_AUTH_REQUEST_AGE: Duration = Duration::from_secs(30);
|
||||
}
|
||||
|
||||
impl Default for Debug {
|
||||
fn default() -> Self {
|
||||
Debug {
|
||||
message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT,
|
||||
maximum_auth_request_age: Self::DEFAULT_MAXIMUM_AUTH_REQUEST_AGE,
|
||||
stale_messages: Default::default(),
|
||||
client_bandwidth: Default::default(),
|
||||
zk_nym_tickets: Default::default(),
|
||||
|
||||
@@ -59,6 +59,7 @@ fn ephemeral_gateway_config(config: &Config) -> nym_gateway::config::Config {
|
||||
.zk_nym_tickets
|
||||
.maximum_time_between_redemption,
|
||||
},
|
||||
maximum_auth_request_age: config.gateway_tasks.debug.maximum_auth_request_age,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user