wip: putting new sphinx keys to self described endpoints
This commit is contained in:
@@ -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
@@ -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" }
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user