wip: putting new sphinx keys to self described endpoints

This commit is contained in:
Jędrzej Stuczyński
2025-05-07 15:06:18 +01:00
parent 440a0dc6a4
commit 3a01d6bf2d
14 changed files with 472 additions and 128 deletions
+59 -4
View File
@@ -857,17 +857,24 @@ impl From<nym_node_requests::api::v1::node::models::HostInformation> 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<SphinxKey>,
#[serde(default)]
#[serde(with = "option_bs58_x25519_pubkey")]
#[schemars(with = "Option<String>")]
@@ -875,16 +882,64 @@ pub struct HostKeys {
pub x25519_noise: Option<x25519::PublicKey>,
}
impl From<nym_node_requests::api::v1::node::models::HostKeys> 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<x25519::PublicKey>,
pub current_x25519_sphinx_key: SphinxKey,
#[serde(default)]
pub pre_announced_x25519_sphinx_key: Option<SphinxKey>,
}
impl From<HostKeysDeHelper> 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<nym_node_requests::api::v1::node::models::SphinxKey> 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<nym_node_requests::api::v1::node::models::HostKeys> 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,
+1 -1
View File
@@ -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" }
+141 -11
View File
@@ -1,7 +1,9 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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,
},
};
@@ -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<IpAddr>,
pub hostname: Option<String>,
pub keys: LegacyHostKeysV3,
}
#[derive(Serialize)]
pub struct LegacyHostInformationV2 {
pub ip_address: Vec<IpAddr>,
@@ -78,14 +85,24 @@ pub struct LegacyHostInformationV2 {
}
#[derive(Serialize)]
pub struct LegacyHostInformation {
pub struct LegacyHostInformationV1 {
pub ip_address: Vec<IpAddr>,
pub hostname: Option<String>,
pub keys: LegacyHostKeys,
pub keys: LegacyHostKeysV1,
}
impl From<HostInformation> for LegacyHostInformationV2 {
impl From<HostInformation> for LegacyHostInformationV3 {
fn from(value: HostInformation) -> Self {
LegacyHostInformationV3 {
ip_address: value.ip_address,
hostname: value.hostname,
keys: value.keys.into(),
}
}
}
impl From<LegacyHostInformationV3> for LegacyHostInformationV2 {
fn from(value: LegacyHostInformationV3) -> Self {
LegacyHostInformationV2 {
ip_address: value.ip_address,
hostname: value.hostname,
@@ -94,9 +111,9 @@ impl From<HostInformation> for LegacyHostInformationV2 {
}
}
impl From<LegacyHostInformationV2> for LegacyHostInformation {
impl From<LegacyHostInformationV2> 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<SphinxKey>,
/// 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<x25519::PublicKey>,
}
#[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<SphinxKey, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum SphinxKeyDeHelper {
Current(SphinxKey),
Legacy(#[serde(with = "bs58_x25519_pubkey")] x25519::PublicKey),
}
impl From<SphinxKeyDeHelper> 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<x25519::PublicKey>,
}
#[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<HostKeys> for LegacyHostKeysV2 {
impl From<HostKeys> 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<LegacyHostKeysV3> 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<HostKeys> for LegacyHostKeysV2 {
}
}
impl From<LegacyHostKeysV2> for LegacyHostKeys {
impl From<LegacyHostKeysV2> for LegacyHostKeysV1 {
fn from(value: LegacyHostKeysV2) -> Self {
LegacyHostKeys {
LegacyHostKeysV1 {
ed25519: value.ed25519_identity,
x25519: value.x25519_sphinx,
}
+29 -38
View File
@@ -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,
}
}
@@ -1396,11 +1396,6 @@ pub async fn try_upgrade_config_v9<P: AsRef<Path>>(
.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
+4 -5
View File
@@ -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)
}
}
@@ -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,
},
};
@@ -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<SphinxPrivateKey>,
/// Optionally, a key from the previous rotation during the overlap period when the keys are rotated.
secondary_key: ArcSwapOption<SphinxPrivateKey>,
/// Optionally, a key for the upcoming rotation that's being pre-announced to other network entities
pre_announced_key: ArcSwapOption<SphinxPrivateKey>,
}
impl ActiveSphinxKeys {
+30 -1
View File
@@ -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:
}
}
+1 -1
View File
@@ -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};
+1 -1
View File
@@ -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;
+52 -12
View File
@@ -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<RwLock<InnerClient>>,
}
struct InnerClient {
active_client: NymApiClient,
available_urls: Vec<Url>,
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<R, T>(
&self,
req: R,
timeout_duration: Duration,
) -> Result<T, NymNodeError>
where
R: AsyncFn(Client) -> Result<T, NymAPIError>,
{
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<B, R>(&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<NymApiClient> for NymApisClient {
impl AsRef<NymApiClient> for InnerClient {
fn as_ref(&self) -> &NymApiClient {
&self.active_client
}
@@ -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,