diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index e12da7afe5..3435717922 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -857,17 +857,24 @@ impl From for HostInf } #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[serde(from = "HostKeysDeHelper")] pub struct HostKeys { #[serde(with = "bs58_ed25519_pubkey")] #[schemars(with = "String")] #[schema(value_type = String)] pub ed25519: ed25519::PublicKey, + #[deprecated(note = "use the current_x25519_sphinx_key with explicit rotation information")] #[serde(with = "bs58_x25519_pubkey")] #[schemars(with = "String")] #[schema(value_type = String)] pub x25519: x25519::PublicKey, + pub current_x25519_sphinx_key: SphinxKey, + + #[serde(default)] + pub pre_announced_x25519_sphinx_key: Option, + #[serde(default)] #[serde(with = "option_bs58_x25519_pubkey")] #[schemars(with = "Option")] @@ -875,16 +882,64 @@ pub struct HostKeys { pub x25519_noise: Option, } -impl From for HostKeys { - fn from(value: nym_node_requests::api::v1::node::models::HostKeys) -> Self { +#[derive(Deserialize)] +struct HostKeysDeHelper { + #[serde(with = "bs58_ed25519_pubkey")] + pub ed25519: ed25519::PublicKey, + + #[serde(default)] + #[serde(with = "option_bs58_x25519_pubkey")] + pub x25519_noise: Option, + + pub current_x25519_sphinx_key: SphinxKey, + + #[serde(default)] + pub pre_announced_x25519_sphinx_key: Option, +} + +impl From for HostKeys { + fn from(value: HostKeysDeHelper) -> Self { HostKeys { - ed25519: value.ed25519_identity, - x25519: value.x25519_sphinx, + ed25519: value.ed25519, + x25519: value.current_x25519_sphinx_key.public_key, + current_x25519_sphinx_key: value.current_x25519_sphinx_key, + pre_announced_x25519_sphinx_key: value.pre_announced_x25519_sphinx_key, x25519_noise: value.x25519_noise, } } } +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct SphinxKey { + pub rotation_id: u32, + + #[serde(with = "bs58_x25519_pubkey")] + #[schemars(with = "String")] + #[schema(value_type = String)] + pub public_key: x25519::PublicKey, +} + +impl From for SphinxKey { + fn from(value: nym_node_requests::api::v1::node::models::SphinxKey) -> Self { + SphinxKey { + rotation_id: value.rotation_id, + public_key: value.public_key, + } + } +} + +impl From for HostKeys { + fn from(value: nym_node_requests::api::v1::node::models::HostKeys) -> Self { + HostKeysDeHelper { + ed25519: value.ed25519_identity, + current_x25519_sphinx_key: value.current_x25519_sphinx_key.into(), + pre_announced_x25519_sphinx_key: value.pre_announced_x25519_sphinx_key.map(Into::into), + x25519_noise: value.x25519_noise, + } + .into() + } +} + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct WebSockets { pub ws_port: u16, diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 4910c24ca8..5d7bbeffbd 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -50,7 +50,7 @@ nym-bin-common = { path = "../common/bin-common", features = [ "basic_tracing", "output_format", ] } -nym-client-core-config-types = { path = "../common/client-core/config-types" } +nym-client-core-config-types = { path = "../common/client-core/config-types", features = ["disk-persistence"] } nym-config = { path = "../common/config" } nym-crypto = { path = "../common/crypto", features = ["asymmetric", "rand"] } nym-nonexhaustive-delayqueue = { path = "../common/nonexhaustive-delayqueue" } diff --git a/nym-node/nym-node-requests/src/api/mod.rs b/nym-node/nym-node-requests/src/api/mod.rs index c2ae5badb4..525cba0cf3 100644 --- a/nym-node/nym-node-requests/src/api/mod.rs +++ b/nym-node/nym-node-requests/src/api/mod.rs @@ -1,7 +1,9 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::api::v1::node::models::{LegacyHostInformation, LegacyHostInformationV2}; +use crate::api::v1::node::models::{ + LegacyHostInformationV1, LegacyHostInformationV2, LegacyHostInformationV3, +}; use crate::error::Error; use nym_crypto::asymmetric::ed25519; use schemars::JsonSchema; @@ -67,8 +69,17 @@ impl SignedHostInformation { } // attempt to verify legacy signatures + let legacy_v3 = SignedData { + data: LegacyHostInformationV3::from(self.data.clone()), + signature: self.signature.clone(), + }; + + if legacy_v3.verify(&self.keys.ed25519_identity) { + return true; + } + let legacy_v2 = SignedData { - data: LegacyHostInformationV2::from(self.data.clone()), + data: LegacyHostInformationV2::from(legacy_v3.data), signature: self.signature.clone(), }; @@ -77,7 +88,7 @@ impl SignedHostInformation { } SignedData { - data: LegacyHostInformation::from(legacy_v2.data), + data: LegacyHostInformationV1::from(legacy_v2.data), signature: self.signature.clone(), } .verify(&self.keys.ed25519_identity) @@ -106,6 +117,7 @@ impl Display for ErrorResponse { #[cfg(test)] mod tests { use super::*; + use crate::api::v1::node::models::{HostKeys, SphinxKey}; use nym_crypto::asymmetric::{ed25519, x25519}; use rand_chacha::rand_core::SeedableRng; @@ -114,14 +126,63 @@ mod tests { let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); let ed22519 = ed25519::KeyPair::new(&mut rng); let x25519_sphinx = x25519::KeyPair::new(&mut rng); + let x25519_sphinx2 = x25519::KeyPair::new(&mut rng); let x25519_noise = x25519::KeyPair::new(&mut rng); + let current_rotation_id = 1234; + + // no pre-announced keys let host_info = crate::api::v1::node::models::HostInformation { ip_address: vec!["1.1.1.1".parse().unwrap()], hostname: Some("foomp.com".to_string()), keys: crate::api::v1::node::models::HostKeys { ed25519_identity: *ed22519.public_key(), - x25519_sphinx: *x25519_sphinx.public_key(), + current_x25519_sphinx_key: SphinxKey { + rotation_id: current_rotation_id, + public_key: *x25519_sphinx.public_key(), + }, + x25519_noise: None, + pre_announced_x25519_sphinx_key: None, + }, + }; + + let signed_info = SignedHostInformation::new(host_info, ed22519.private_key()).unwrap(); + assert!(signed_info.verify(ed22519.public_key())); + assert!(signed_info.verify_host_information()); + + let host_info_with_noise = crate::api::v1::node::models::HostInformation { + ip_address: vec!["1.1.1.1".parse().unwrap()], + hostname: Some("foomp.com".to_string()), + keys: crate::api::v1::node::models::HostKeys { + ed25519_identity: *ed22519.public_key(), + current_x25519_sphinx_key: SphinxKey { + rotation_id: current_rotation_id, + public_key: *x25519_sphinx.public_key(), + }, + pre_announced_x25519_sphinx_key: None, + x25519_noise: Some(*x25519_noise.public_key()), + }, + }; + + let signed_info = + SignedHostInformation::new(host_info_with_noise, ed22519.private_key()).unwrap(); + assert!(signed_info.verify(ed22519.public_key())); + assert!(signed_info.verify_host_information()); + + // with pre-announced keys + let host_info = crate::api::v1::node::models::HostInformation { + ip_address: vec!["1.1.1.1".parse().unwrap()], + hostname: Some("foomp.com".to_string()), + keys: crate::api::v1::node::models::HostKeys { + ed25519_identity: *ed22519.public_key(), + current_x25519_sphinx_key: SphinxKey { + rotation_id: current_rotation_id, + public_key: *x25519_sphinx.public_key(), + }, + pre_announced_x25519_sphinx_key: Some(SphinxKey { + rotation_id: current_rotation_id + 1, + public_key: *x25519_sphinx2.public_key(), + }), x25519_noise: None, }, }; @@ -135,7 +196,14 @@ mod tests { hostname: Some("foomp.com".to_string()), keys: crate::api::v1::node::models::HostKeys { ed25519_identity: *ed22519.public_key(), - x25519_sphinx: *x25519_sphinx.public_key(), + current_x25519_sphinx_key: SphinxKey { + rotation_id: current_rotation_id, + public_key: *x25519_sphinx.public_key(), + }, + pre_announced_x25519_sphinx_key: Some(SphinxKey { + rotation_id: current_rotation_id + 1, + public_key: *x25519_sphinx2.public_key(), + }), x25519_noise: Some(*x25519_noise.public_key()), }, }; @@ -146,6 +214,53 @@ mod tests { assert!(signed_info.verify_host_information()); } + #[test] + fn dummy_legacy_v3_signed_host_verification() { + let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); + let ed22519 = ed25519::KeyPair::new(&mut rng); + let x25519_sphinx = x25519::KeyPair::new(&mut rng); + let x25519_noise = x25519::KeyPair::new(&mut rng); + + let legacy_info = crate::api::v1::node::models::LegacyHostInformationV3 { + ip_address: vec!["1.1.1.1".parse().unwrap()], + hostname: Some("foomp.com".to_string()), + keys: crate::api::v1::node::models::LegacyHostKeysV3 { + ed25519_identity: *ed22519.public_key(), + x25519_sphinx: *x25519_sphinx.public_key(), + x25519_noise: Some(*x25519_noise.public_key()), + }, + }; + + // note the usage of u32::max rotation id (as that's what the legacy data would be deserialised into) + let current_struct = crate::api::v1::node::models::HostInformation { + ip_address: vec!["1.1.1.1".parse().unwrap()], + hostname: Some("foomp.com".to_string()), + keys: HostKeys { + ed25519_identity: *ed22519.public_key(), + current_x25519_sphinx_key: SphinxKey { + rotation_id: u32::MAX, + public_key: *x25519_sphinx.public_key(), + }, + pre_announced_x25519_sphinx_key: None, + x25519_noise: Some(*x25519_noise.public_key()), + }, + }; + + // signature on legacy data + let signature = SignedData::new(legacy_info, ed22519.private_key()) + .unwrap() + .signature; + + // signed blob with the 'current' structure + let current_struct = SignedData { + data: current_struct, + signature, + }; + + assert!(!current_struct.verify(ed22519.public_key())); + assert!(current_struct.verify_host_information()) + } + #[test] fn dummy_legacy_v2_signed_host_verification() { let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); @@ -173,22 +288,32 @@ mod tests { }, }; + // note the usage of u32::max rotation id (as that's what the legacy data would be deserialised into) let host_info_no_noise = crate::api::v1::node::models::HostInformation { ip_address: legacy_info_no_noise.ip_address.clone(), hostname: legacy_info_no_noise.hostname.clone(), keys: crate::api::v1::node::models::HostKeys { ed25519_identity: legacy_info_no_noise.keys.ed25519_identity.parse().unwrap(), - x25519_sphinx: legacy_info_no_noise.keys.x25519_sphinx.parse().unwrap(), + current_x25519_sphinx_key: SphinxKey { + rotation_id: u32::MAX, + public_key: *x25519_sphinx.public_key(), + }, + pre_announced_x25519_sphinx_key: None, x25519_noise: None, }, }; + // note the usage of u32::max rotation id (as that's what the legacy data would be deserialised into) let host_info_noise = crate::api::v1::node::models::HostInformation { ip_address: legacy_info_noise.ip_address.clone(), hostname: legacy_info_noise.hostname.clone(), keys: crate::api::v1::node::models::HostKeys { ed25519_identity: legacy_info_noise.keys.ed25519_identity.parse().unwrap(), - x25519_sphinx: legacy_info_noise.keys.x25519_sphinx.parse().unwrap(), + current_x25519_sphinx_key: SphinxKey { + rotation_id: u32::MAX, + public_key: *x25519_sphinx.public_key(), + }, + pre_announced_x25519_sphinx_key: None, x25519_noise: Some(legacy_info_noise.keys.x25519_noise.parse().unwrap()), }, }; @@ -222,26 +347,31 @@ mod tests { } #[test] - fn dummy_legacy_signed_host_verification() { + fn dummy_legacy_v1_signed_host_verification() { let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); let ed22519 = ed25519::KeyPair::new(&mut rng); let x25519_sphinx = x25519::KeyPair::new(&mut rng); - let legacy_info = crate::api::v1::node::models::LegacyHostInformation { + let legacy_info = crate::api::v1::node::models::LegacyHostInformationV1 { ip_address: vec!["1.1.1.1".parse().unwrap()], hostname: Some("foomp.com".to_string()), - keys: crate::api::v1::node::models::LegacyHostKeys { + keys: crate::api::v1::node::models::LegacyHostKeysV1 { ed25519: ed22519.public_key().to_base58_string(), x25519: x25519_sphinx.public_key().to_base58_string(), }, }; + // note the usage of u32::max rotation id (as that's what the legacy data would be deserialised into) let host_info = crate::api::v1::node::models::HostInformation { ip_address: legacy_info.ip_address.clone(), hostname: legacy_info.hostname.clone(), keys: crate::api::v1::node::models::HostKeys { ed25519_identity: legacy_info.keys.ed25519.parse().unwrap(), - x25519_sphinx: legacy_info.keys.x25519.parse().unwrap(), + current_x25519_sphinx_key: SphinxKey { + rotation_id: u32::MAX, + public_key: *x25519_sphinx.public_key(), + }, + pre_announced_x25519_sphinx_key: None, x25519_noise: None, }, }; diff --git a/nym-node/nym-node-requests/src/api/v1/node/models.rs b/nym-node/nym-node-requests/src/api/v1/node/models.rs index 5f047912fa..862852826b 100644 --- a/nym-node/nym-node-requests/src/api/v1/node/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/node/models.rs @@ -8,7 +8,7 @@ use nym_crypto::asymmetric::x25519::{ serde_helpers::{bs58_x25519_pubkey, option_bs58_x25519_pubkey}, }; use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use std::net::IpAddr; pub use crate::api::SignedHostInformation; @@ -70,6 +70,13 @@ impl HostInformation { } } +#[derive(Serialize)] +pub struct LegacyHostInformationV3 { + pub ip_address: Vec, + pub hostname: Option, + pub keys: LegacyHostKeysV3, +} + #[derive(Serialize)] pub struct LegacyHostInformationV2 { pub ip_address: Vec, @@ -78,14 +85,24 @@ pub struct LegacyHostInformationV2 { } #[derive(Serialize)] -pub struct LegacyHostInformation { +pub struct LegacyHostInformationV1 { pub ip_address: Vec, pub hostname: Option, - pub keys: LegacyHostKeys, + pub keys: LegacyHostKeysV1, } -impl From for LegacyHostInformationV2 { +impl From for LegacyHostInformationV3 { fn from(value: HostInformation) -> Self { + LegacyHostInformationV3 { + ip_address: value.ip_address, + hostname: value.hostname, + keys: value.keys.into(), + } + } +} + +impl From for LegacyHostInformationV2 { + fn from(value: LegacyHostInformationV3) -> Self { LegacyHostInformationV2 { ip_address: value.ip_address, hostname: value.hostname, @@ -94,9 +111,9 @@ impl From for LegacyHostInformationV2 { } } -impl From for LegacyHostInformation { +impl From for LegacyHostInformationV1 { fn from(value: LegacyHostInformationV2) -> Self { - LegacyHostInformation { + LegacyHostInformationV1 { ip_address: value.ip_address, hostname: value.hostname, keys: value.keys.into(), @@ -114,13 +131,16 @@ pub struct HostKeys { #[cfg_attr(feature = "openapi", schema(value_type = String))] pub ed25519_identity: ed25519::PublicKey, - /// Base58-encoded x25519 public key of this node used for sphinx/outfox packet creation. - /// Currently, it corresponds to either mixnode's or gateway's key. + /// Current, active, x25519 sphinx key clients are expected to use when constructing packets + /// with this node in the route. #[serde(alias = "x25519")] - #[serde(with = "bs58_x25519_pubkey")] - #[schemars(with = "String")] - #[cfg_attr(feature = "openapi", schema(value_type = String))] - pub x25519_sphinx: x25519::PublicKey, + #[serde(alias = "x25519_sphinx")] + #[serde(deserialize_with = "de_sphinx_key")] + pub current_x25519_sphinx_key: SphinxKey, + + /// Pre-announced x25519 sphinx key clients will use during the following key rotation + #[serde(default)] + pub pre_announced_x25519_sphinx_key: Option, /// Base58-encoded x25519 public key of this node used for the noise protocol. #[serde(default)] @@ -130,6 +150,62 @@ pub struct HostKeys { pub x25519_noise: Option, } +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct SphinxKey { + pub rotation_id: u32, + + #[serde(with = "bs58_x25519_pubkey")] + #[schemars(with = "String")] + #[cfg_attr(feature = "openapi", schema(value_type = String))] + pub public_key: x25519::PublicKey, +} + +fn de_sphinx_key<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum SphinxKeyDeHelper { + Current(SphinxKey), + Legacy(#[serde(with = "bs58_x25519_pubkey")] x25519::PublicKey), + } + + impl From for SphinxKey { + fn from(value: SphinxKeyDeHelper) -> Self { + match value { + SphinxKeyDeHelper::Current(key) => key, + SphinxKeyDeHelper::Legacy(public_key) => SphinxKey { + rotation_id: u32::MAX, + public_key, + }, + } + } + } + + Ok(SphinxKeyDeHelper::deserialize(deserializer)?.into()) +} + +#[derive(Serialize)] +pub struct LegacyHostKeysV3 { + /// Base58-encoded ed25519 public key of this node. Currently, it corresponds to either mixnode's or gateway's identity. + #[serde(alias = "ed25519")] + #[serde(with = "bs58_ed25519_pubkey")] + pub ed25519_identity: ed25519::PublicKey, + + /// Base58-encoded x25519 public key of this node used for sphinx/outfox packet creation. + /// Currently, it corresponds to either mixnode's or gateway's key. + #[serde(alias = "x25519")] + #[serde(with = "bs58_x25519_pubkey")] + pub x25519_sphinx: x25519::PublicKey, + + /// Base58-encoded x25519 public key of this node used for the noise protocol. + #[serde(default)] + #[serde(with = "option_bs58_x25519_pubkey")] + pub x25519_noise: Option, +} + #[derive(Serialize)] pub struct LegacyHostKeysV2 { pub ed25519_identity: String, @@ -138,13 +214,23 @@ pub struct LegacyHostKeysV2 { } #[derive(Serialize)] -pub struct LegacyHostKeys { +pub struct LegacyHostKeysV1 { pub ed25519: String, pub x25519: String, } -impl From for LegacyHostKeysV2 { +impl From for LegacyHostKeysV3 { fn from(value: HostKeys) -> Self { + LegacyHostKeysV3 { + ed25519_identity: value.ed25519_identity, + x25519_sphinx: value.current_x25519_sphinx_key.public_key, + x25519_noise: value.x25519_noise, + } + } +} + +impl From for LegacyHostKeysV2 { + fn from(value: LegacyHostKeysV3) -> Self { LegacyHostKeysV2 { ed25519_identity: value.ed25519_identity.to_base58_string(), x25519_sphinx: value.x25519_sphinx.to_base58_string(), @@ -156,9 +242,9 @@ impl From for LegacyHostKeysV2 { } } -impl From for LegacyHostKeys { +impl From for LegacyHostKeysV1 { fn from(value: LegacyHostKeysV2) -> Self { - LegacyHostKeys { + LegacyHostKeysV1 { ed25519: value.ed25519_identity, x25519: value.x25519_sphinx, } diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index cc7fd21886..b529545873 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -639,12 +639,6 @@ pub struct ReplayProtectionDebug { /// It's performed in case the traffic rates increase before the next bloomfilter update. pub bloomfilter_size_multiplier: f64, - // NOTE: this field is temporary until replay detection bloomfilter rotation is tied - // to key rotation - /// Specifies how often the bloomfilter is cleared - #[serde(with = "humantime_serde")] - pub bloomfilter_reset_rate: Duration, - /// Specifies how often the bloomfilter is flushed to disk for recovery in case of a crash #[serde(with = "humantime_serde")] pub bloomfilter_disk_flushing_rate: Duration, @@ -661,9 +655,6 @@ impl ReplayProtectionDebug { // 10^-5 pub const DEFAULT_REPLAY_DETECTION_FALSE_POSITIVE_RATE: f64 = 1e-5; - // 25h (key rotation will be happening every 24h + 1h of overlap) - pub const DEFAULT_REPLAY_DETECTION_BF_RESET_RATE: Duration = Duration::from_secs(25 * 60 * 60); - // we must have some reasonable balance between losing values and trashing the disk. // since on average HDD it would take ~30s to save a 2GB bloomfilter pub const DEFAULT_BF_DISK_FLUSHING_RATE: Duration = Duration::from_secs(10 * 60); @@ -674,34 +665,35 @@ impl ReplayProtectionDebug { pub const DEFAULT_BLOOMFILTER_MINIMUM_PACKETS_PER_SECOND_SIZE: usize = 200; pub fn validate(&self) -> Result<(), NymNodeError> { - if self.false_positive_rate >= 1.0 || self.false_positive_rate <= 0.0 { - return Err(NymNodeError::config_validation_failure( - "false positive rate for replay detection can't be larger than (or equal to) 1 or smaller than (or equal to) 0", - )); - } - - let items_in_filter = items_in_bloomfilter( - self.bloomfilter_reset_rate, - self.initial_expected_packets_per_second, - ); - let bitmap_size = bitmap_size(self.false_positive_rate, items_in_filter); - let bloomfilter_size = bitmap_size / 8; - - let mut sys_info = System::new(); - sys_info.refresh_memory(); - - // we'll need 2x size of the bloomfilter - // as during key transition we'll have to simultaneously use two filters - // plus we also need to make a memcopy during disk flush - let required_memory = 2 * bloomfilter_size; - - let memory = sys_info.available_memory(); - if (memory as usize) < required_memory { - return Err(NymNodeError::config_validation_failure( - format!("system does not have sufficient memory to allocate required replay protection bloomfilters. {} is available whilst at least {} is needed",memory.human_count_bytes(), required_memory.human_count_bytes()))); - } - - Ok(()) + todo!() + // if self.false_positive_rate >= 1.0 || self.false_positive_rate <= 0.0 { + // return Err(NymNodeError::config_validation_failure( + // "false positive rate for replay detection can't be larger than (or equal to) 1 or smaller than (or equal to) 0", + // )); + // } + // + // let items_in_filter = items_in_bloomfilter( + // self.bloomfilter_reset_rate, + // self.initial_expected_packets_per_second, + // ); + // let bitmap_size = bitmap_size(self.false_positive_rate, items_in_filter); + // let bloomfilter_size = bitmap_size / 8; + // + // let mut sys_info = System::new(); + // sys_info.refresh_memory(); + // + // // we'll need 2x size of the bloomfilter + // // as during key transition we'll have to simultaneously use two filters + // // plus we also need to make a memcopy during disk flush + // let required_memory = 2 * bloomfilter_size; + // + // let memory = sys_info.available_memory(); + // if (memory as usize) < required_memory { + // return Err(NymNodeError::config_validation_failure( + // format!("system does not have sufficient memory to allocate required replay protection bloomfilters. {} is available whilst at least {} is needed",memory.human_count_bytes(), required_memory.human_count_bytes()))); + // } + // + // Ok(()) } } @@ -717,7 +709,6 @@ impl Default for ReplayProtectionDebug { bloomfilter_minimum_packets_per_second_size: Self::DEFAULT_BLOOMFILTER_MINIMUM_PACKETS_PER_SECOND_SIZE, bloomfilter_size_multiplier: Self::DEFAULT_BLOOMFILTER_SIZE_MULTIPLIER, - bloomfilter_reset_rate: Self::DEFAULT_REPLAY_DETECTION_BF_RESET_RATE, bloomfilter_disk_flushing_rate: Self::DEFAULT_BF_DISK_FLUSHING_RATE, } } diff --git a/nym-node/src/config/old_configs/old_config_v9.rs b/nym-node/src/config/old_configs/old_config_v9.rs index aa968899f1..23c96a5cb8 100644 --- a/nym-node/src/config/old_configs/old_config_v9.rs +++ b/nym-node/src/config/old_configs/old_config_v9.rs @@ -1396,11 +1396,6 @@ pub async fn try_upgrade_config_v9>( .replay_protection .debug .bloomfilter_size_multiplier, - bloomfilter_reset_rate: old_cfg - .mixnet - .replay_protection - .debug - .bloomfilter_reset_rate, bloomfilter_disk_flushing_rate: old_cfg .mixnet .replay_protection diff --git a/nym-node/src/config/persistence.rs b/nym-node/src/config/persistence.rs index daab9af610..57da186548 100644 --- a/nym-node/src/config/persistence.rs +++ b/nym-node/src/config/persistence.rs @@ -59,7 +59,6 @@ pub const DEFAULT_X25519_WG_PUBLIC_DH_KEY_FILENAME: &str = "x25519_wg_dh.pub"; pub const DEFAULT_RD_BLOOMFILTER_SUBDIR: &str = "replay-detection"; pub const DEFAULT_RD_BLOOMFILTER_FILE_EXT: &str = "bloom"; pub const DEFAULT_RD_BLOOMFILTER_FLUSH_FILE_EXT: &str = "flush"; -pub const CURRENT_RD_BLOOMFILTER_FILENAME: &str = "current"; #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] @@ -498,15 +497,15 @@ pub struct ReplayProtectionPaths { } impl ReplayProtectionPaths { - pub fn current_bloomfilter_filepath(&self) -> PathBuf { + pub fn bloomfilter_filepath(&self, rotation_id: u32) -> PathBuf { self.current_bloomfilters_directory - .join(CURRENT_RD_BLOOMFILTER_FILENAME) + .join(format!("rot-{rotation_id}")) .with_extension(DEFAULT_RD_BLOOMFILTER_FILE_EXT) } - pub fn current_bloomfilter_being_flushed_filepath(&self) -> PathBuf { + pub fn current_bloomfilter_being_flushed_filepath(&self, rotation_id: u32) -> PathBuf { self.current_bloomfilters_directory - .join(CURRENT_RD_BLOOMFILTER_FILENAME) + .join(format!("rot-{rotation_id}")) .with_extension(DEFAULT_RD_BLOOMFILTER_FLUSH_FILE_EXT) } } diff --git a/nym-node/src/node/http/router/api/v1/node/host_information.rs b/nym-node/src/node/http/router/api/v1/node/host_information.rs index ebe5d0c2ba..a03ceb993a 100644 --- a/nym-node/src/node/http/router/api/v1/node/host_information.rs +++ b/nym-node/src/node/http/router/api/v1/node/host_information.rs @@ -3,6 +3,7 @@ use crate::node::http::api::api_requests; use crate::node::http::state::AppState; +use crate::node::key_rotation::active_keys::SphinxKeyGuard; use axum::extract::{Query, State}; use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_node_requests::api::{v1::node::models::SignedHostInformation, SignedDataHostInfo}; @@ -27,13 +28,25 @@ pub(crate) async fn host_information( ) -> HostInformationResponse { let output = output.output.unwrap_or_default(); + let primary_key = state.x25519_sphinx_keys.primary(); + let pre_announced = match state.x25519_sphinx_keys.secondary() { + None => None, + Some(secondary_key) => { + todo!() + } + }; + let host_info = api_requests::v1::node::models::HostInformation { ip_address: state.static_information.ip_addresses.clone(), hostname: state.static_information.hostname.clone(), keys: api_requests::v1::node::models::HostKeys { ed25519_identity: *state.static_information.ed25519_identity_keys.public_key(), - x25519_sphinx: state.x25519_sphinx_keys.primary().x25519_pubkey(), + current_x25519_sphinx_key: api_requests::v1::node::models::SphinxKey { + rotation_id: primary_key.rotation_id(), + public_key: primary_key.x25519_pubkey(), + }, x25519_noise: state.static_information.x25519_noise_key, + pre_announced_x25519_sphinx_key: pre_announced, }, }; diff --git a/nym-node/src/node/key_rotation/active_keys.rs b/nym-node/src/node/key_rotation/active_keys.rs index 4882d04c6c..cb2dcba51f 100644 --- a/nym-node/src/node/key_rotation/active_keys.rs +++ b/nym-node/src/node/key_rotation/active_keys.rs @@ -13,8 +13,14 @@ pub(crate) struct ActiveSphinxKeys { } struct ActiveSphinxKeysInner { + /// Key that's currently used as the default when processing packets with no explicit rotation information primary_key: ArcSwap, + + /// Optionally, a key from the previous rotation during the overlap period when the keys are rotated. secondary_key: ArcSwapOption, + + /// Optionally, a key for the upcoming rotation that's being pre-announced to other network entities + pre_announced_key: ArcSwapOption, } impl ActiveSphinxKeys { diff --git a/nym-node/src/node/key_rotation/controller.rs b/nym-node/src/node/key_rotation/controller.rs index 752bf706e4..fdd76afece 100644 --- a/nym-node/src/node/key_rotation/controller.rs +++ b/nym-node/src/node/key_rotation/controller.rs @@ -2,25 +2,54 @@ // SPDX-License-Identifier: Apache-2.0 use crate::node::key_rotation::manager::SphinxKeyManager; +use crate::node::nym_apis_client::NymApisClient; use nym_task::ShutdownToken; -use tokio::io::AsyncWriteExt; +use std::time::Duration; +use tokio::time::interval; use tracing::{info, trace}; pub(crate) struct KeyRotationController { + // regular polling rate to catch any changes in the system config. they shouldn't happen too often + // so the requests can be sent quite infrequently + regular_polling_interval: Duration, + + client: NymApisClient, managed_keys: SphinxKeyManager, shutdown_token: ShutdownToken, } +enum KeyRotationActionState { + // perform key-rotation and pre-announce new key to the nym-api(s) + PreAnnounce, + + // remove the old key and purge associated data like the replay detection bloomfilter + PurgeOld, +} + impl KeyRotationController { + pub(crate) fn new(client: NymApisClient, shutdown_token: ShutdownToken) -> Self { + todo!() + } + + async fn regular_poll(&self) { + todo!() + } + pub(crate) async fn run(&self) { info!("starting sphinx key rotation controller"); + let mut polling_interval = interval(self.regular_polling_interval); + polling_interval.reset(); + while !self.shutdown_token.is_cancelled() { tokio::select! { biased; _ = self.shutdown_token.cancelled() => { trace!("KeyRotationController: Received shutdown"); } + _ = polling_interval.tick() => { + self.regular_poll().await; + } // TODO: } } diff --git a/nym-node/src/node/key_rotation/manager.rs b/nym-node/src/node/key_rotation/manager.rs index 95d448ede4..031de7d06f 100644 --- a/nym-node/src/node/key_rotation/manager.rs +++ b/nym-node/src/node/key_rotation/manager.rs @@ -5,8 +5,8 @@ use crate::error::{KeyIOFailure, NymNodeError}; use crate::node::helpers::{load_key, store_key}; use crate::node::key_rotation::active_keys::ActiveSphinxKeys; use crate::node::key_rotation::key::SphinxPrivateKey; -use nym_crypto::aes::cipher::crypto_common::rand_core::{CryptoRng, RngCore}; use rand::rngs::OsRng; +use rand::{CryptoRng, RngCore}; use std::fs; use std::path::{Path, PathBuf}; use tracing::{trace, warn}; diff --git a/nym-node/src/node/key_rotation/mod.rs b/nym-node/src/node/key_rotation/mod.rs index 79bbe9cef6..9e89bc4d28 100644 --- a/nym-node/src/node/key_rotation/mod.rs +++ b/nym-node/src/node/key_rotation/mod.rs @@ -2,6 +2,6 @@ // SPDX-License-Identifier: GPL-3.0-only pub(crate) mod active_keys; -mod controller; +pub(crate) mod controller; pub(crate) mod key; pub(crate) mod manager; diff --git a/nym-node/src/node/nym_apis_client.rs b/nym-node/src/node/nym_apis_client.rs index 9949078343..61794033de 100644 --- a/nym-node/src/node/nym_apis_client.rs +++ b/nym-node/src/node/nym_apis_client.rs @@ -12,12 +12,19 @@ use nym_validator_client::nym_api::error::NymAPIError; use nym_validator_client::NymApiClient; use rand::prelude::SliceRandom; use rand::thread_rng; +use std::sync::Arc; use std::time::Duration; +use tokio::sync::RwLock; use tokio::time::timeout; use tracing::warn; use url::Url; +#[derive(Clone)] pub struct NymApisClient { + inner: Arc>, +} + +struct InnerClient { active_client: NymApiClient, available_urls: Vec, currently_used_api: usize, @@ -38,24 +45,57 @@ impl NymApisClient { .build()?; Ok(NymApisClient { - active_client: NymApiClient { - nym_api: active_client, - }, - available_urls: urls, - currently_used_api: 0, + inner: Arc::new(RwLock::new(InnerClient { + active_client: NymApiClient { + nym_api: active_client, + }, + available_urls: urls, + currently_used_api: 0, + })), }) } - fn use_next_endpoint(&mut self) { - if self.available_urls.len() == 1 { + async fn use_next_endpoint(&mut self) { + let mut guard = self.inner.write().await; + if guard.available_urls.len() == 1 { return; } - self.currently_used_api = (self.currently_used_api + 1) % self.available_urls.len(); - self.active_client - .change_nym_api(self.available_urls[self.currently_used_api].clone()) + let next_index = (guard.currently_used_api + 1) % guard.available_urls.len(); + let next = guard.available_urls[next_index].clone(); + guard.currently_used_api = next_index; + guard.active_client.change_nym_api(next) } + pub(crate) async fn query_exhaustively( + &self, + req: R, + timeout_duration: Duration, + ) -> Result + where + R: AsyncFn(Client) -> Result, + { + self.inner + .read() + .await + .query_exhaustively(req, timeout_duration) + .await + } + + pub(crate) async fn broadcast_force_refresh(&self, private_key: &ed25519::PrivateKey) { + self.inner + .read() + .await + .broadcast_force_refresh(private_key) + .await; + } + + pub(crate) async fn broadcast_key_rotation(&self) { + self.inner.read().await.broadcast_key_rotation().await; + } +} + +impl InnerClient { // currently there are no cases without json body, but for those we'd just need to slightly adjust the signature async fn broadcast(&self, request_body: &B, req: R, timeout_duration: Duration) where @@ -115,11 +155,11 @@ impl NymApisClient { } pub(crate) async fn broadcast_key_rotation(&self) { - // + todo!() } } -impl AsRef for NymApisClient { +impl AsRef for InnerClient { fn as_ref(&self) -> &NymApiClient { &self.active_client } diff --git a/nym-node/src/node/replay_protection/background_task.rs b/nym-node/src/node/replay_protection/background_task.rs index 392c79cd68..37b5463a5f 100644 --- a/nym-node/src/node/replay_protection/background_task.rs +++ b/nym-node/src/node/replay_protection/background_task.rs @@ -33,41 +33,41 @@ struct ReplayProtectionBackgroundTaskConfig { impl From<&Config> for ReplayProtectionBackgroundTaskConfig { fn from(config: &Config) -> Self { - ReplayProtectionBackgroundTaskConfig { - current_bloomfilter_path: config - .mixnet - .replay_protection - .storage_paths - .current_bloomfilter_filepath(), - current_bloomfilter_temp_flush_path: config - .mixnet - .replay_protection - .storage_paths - .current_bloomfilter_being_flushed_filepath(), - false_positive_rate: config.mixnet.replay_protection.debug.false_positive_rate, - filter_reset_rate: config.mixnet.replay_protection.debug.bloomfilter_reset_rate, - disk_flushing_rate: config - .mixnet - .replay_protection - .debug - .bloomfilter_disk_flushing_rate, - bloomfilter_size_multiplier: config - .mixnet - .replay_protection - .debug - .bloomfilter_size_multiplier, - minimum_bloomfilter_packets_per_second: config - .mixnet - .replay_protection - .debug - .bloomfilter_minimum_packets_per_second_size, - } + todo!() + // ReplayProtectionBackgroundTaskConfig { + // current_bloomfilter_path: config + // .mixnet + // .replay_protection + // .storage_paths + // .current_bloomfilter_filepath(), + // current_bloomfilter_temp_flush_path: config + // .mixnet + // .replay_protection + // .storage_paths + // .current_bloomfilter_being_flushed_filepath(), + // false_positive_rate: config.mixnet.replay_protection.debug.false_positive_rate, + // filter_reset_rate: config.mixnet.replay_protection.debug.bloomfilter_reset_rate, + // disk_flushing_rate: config + // .mixnet + // .replay_protection + // .debug + // .bloomfilter_disk_flushing_rate, + // bloomfilter_size_multiplier: config + // .mixnet + // .replay_protection + // .debug + // .bloomfilter_size_multiplier, + // minimum_bloomfilter_packets_per_second: config + // .mixnet + // .replay_protection + // .debug + // .bloomfilter_minimum_packets_per_second_size, + // } } } -// background task responsible for periodically flushing the bloomfilter to disk -// as well as clearing it up on the specified timer -// (in the future this will be enforced by key rotation) +// background task responsible for periodically flushing the bloomfilters to disk +// it no longer removes them on the timer as it's now responsibility of the key rotation controller pub struct ReplayProtectionBackgroundTask { config: ReplayProtectionBackgroundTaskConfig, last_reset: LastResetData,