feat: key rotation (#5777)

* wip

* wip: wrap node's sphinx key with a manager

* wip: choosing correct key for packet processing

* further propagation of key rotation information

* attaching key rotation information to reply surbs

* added basic key rotation information to mixnet contract

* wip: introducing cached queries for key rotation info from nym api

* unified nym-api contract cache refreshing

* finish packet decoding

* multi api client + retrieving rotation id

* rotating sphinx key files

* logic for migrating config file

* wip: putting new sphinx keys to self described endpoints

* processing loop of KeyRotationController

* fixed sphinx key loading

* rotating bloomfilters

* wired up KeyRotationController

* flushing bloomfilters to disk and loading

* most of nym-node changes

* post rebase fixes

* fixes due to backwards compatible hostkeys

* split http state.rs file

* dont use deprecated fields

* fixed backwards compatible deserialisation of host information

* split up node describe cache

* added a dedicated CacheRefresher listener to perform full refresh outside the set interval

* controlling announced sphinx keys within nym-api

* retrieving rotation id when pulling topology

* split nym-nodes http handlers

* v2 nym-api endpoints to retrieve nodes with additional metadata information

* bug fixes...

* additional bugfixes and guards against stuck epoch

* testnet manager: set first nym-api as the rewarder

* fixed host information deserialisation

* fixed panic during first key rotation

* post rebase fixes

* clippy

* more guards against stuck epochs

* added helper method to reset node's sphinx key

* instantiate mixnet contract with custom key rotation validity

* additional bugfixes and debugging nym-api deadlock

* passing shutdown to nym apis client

* remove dead test

* post rebasing fixes

* missing MixnetQueryClient variants

* remove usage of deprecated methods in sdk example

* fix: incorrect method signature

* post rebasing fixes

* attempt to retrieve key rotation id before doing any config migration work

* ignore tests relying on networking behaviour

* allow networking failures in certain tests
This commit is contained in:
Jędrzej Stuczyński
2025-06-03 12:22:51 +02:00
committed by GitHub
parent adbe0392ca
commit d8c84cc4d6
204 changed files with 9392 additions and 3819 deletions
-34
View File
@@ -1,38 +1,4 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::config::Config;
use crate::error::NymNodeError;
use crate::node::http::api::api_requests;
use crate::node::http::error::NymNodeHttpError;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_node_requests::api::SignedHostInformation;
pub mod system_info;
pub(crate) fn sign_host_details(
config: &Config,
x22519_sphinx: &x25519::PublicKey,
x25519_noise: &x25519::PublicKey,
ed22519_identity: &ed25519::KeyPair,
) -> Result<SignedHostInformation, NymNodeError> {
let x25519_noise = if config.mixnet.debug.unsafe_disable_noise {
None
} else {
Some(*x25519_noise)
};
let host_info = api_requests::v1::node::models::HostInformation {
ip_address: config.host.public_ips.clone(),
hostname: config.host.hostname.clone(),
keys: api_requests::v1::node::models::HostKeys {
ed25519_identity: *ed22519_identity.public_key(),
x25519_sphinx: *x22519_sphinx,
x25519_noise,
},
};
let signed_info = SignedHostInformation::new(host_info, ed22519_identity.private_key())
.map_err(NymNodeHttpError::from)?;
Ok(signed_info)
}
@@ -1,7 +1,9 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use axum::extract::Query;
use crate::node::http::api::api_requests;
use crate::node::http::state::AppState;
use axum::extract::{Query, State};
use nym_http_api_common::{FormattedResponse, OutputParams};
use nym_node_requests::api::{v1::node::models::SignedHostInformation, SignedDataHostInfo};
@@ -20,11 +22,54 @@ use nym_node_requests::api::{v1::node::models::SignedHostInformation, SignedData
params(OutputParams)
)]
pub(crate) async fn host_information(
host_information: SignedHostInformation,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> HostInformationResponse {
let output = output.output.unwrap_or_default();
output.to_response(host_information)
let primary_key = state.x25519_sphinx_keys.primary();
let pre_announced = match state.x25519_sphinx_keys.secondary() {
None => None,
Some(secondary_key) => {
if secondary_key.rotation_id() == primary_key.rotation_id() + 1 {
Some(api_requests::v1::node::models::SphinxKey {
rotation_id: secondary_key.rotation_id(),
public_key: secondary_key.x25519_pubkey(),
})
} else {
None
}
}
};
let primary_pubkey = primary_key.x25519_pubkey();
#[allow(deprecated)]
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: primary_pubkey,
primary_x25519_sphinx_key: api_requests::v1::node::models::SphinxKey {
rotation_id: primary_key.rotation_id(),
public_key: primary_pubkey,
},
x25519_noise: state.static_information.x25519_noise_key,
pre_announced_x25519_sphinx_key: pre_announced,
},
};
// SAFETY: the only way for this call to fail is if serialisation of HostInformation fails.
// however, that conversion is stable and infallible
#[allow(clippy::unwrap_used)]
let signed_info = SignedHostInformation::new(
host_info,
state.static_information.ed25519_identity_keys.private_key(),
)
.unwrap();
output.to_response(signed_info)
}
pub type HostInformationResponse = FormattedResponse<SignedHostInformation>;
@@ -7,6 +7,7 @@ use crate::node::http::api::v1::node::description::description;
use crate::node::http::api::v1::node::hardware::host_system;
use crate::node::http::api::v1::node::host_information::host_information;
use crate::node::http::api::v1::node::roles::roles;
use crate::node::http::state::AppState;
use axum::routing::get;
use axum::Router;
use nym_node_requests::api::v1::node::models;
@@ -22,14 +23,13 @@ pub mod roles;
#[derive(Debug, Clone)]
pub struct Config {
pub build_information: models::BinaryBuildInformationOwned,
pub host_information: models::SignedHostInformation,
pub system_info: Option<models::HostSystem>,
pub roles: models::NodeRoles,
pub description: models::NodeDescription,
pub auxiliary_details: models::AuxiliaryDetails,
}
pub(super) fn routes<S: Send + Sync + 'static + Clone>(config: Config) -> Router<S> {
pub(super) fn routes(config: Config) -> Router<AppState> {
Router::new()
.route(
v1::BUILD_INFO,
@@ -45,13 +45,7 @@ pub(super) fn routes<S: Send + Sync + 'static + Clone>(config: Config) -> Router
move |query| roles(node_roles, query)
}),
)
.route(
v1::HOST_INFO,
get({
let host_info = config.host_information;
move |query| host_information(host_info, query)
}),
)
.route(v1::HOST_INFO, get(host_information))
.route(
v1::SYSTEM_INFO,
get({
+2 -3
View File
@@ -16,7 +16,6 @@ use nym_node_requests::api::v1::mixnode::models::Mixnode;
use nym_node_requests::api::v1::network_requester::exit_policy::models::UsedExitPolicy;
use nym_node_requests::api::v1::network_requester::models::NetworkRequester;
use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, HostSystem, NodeDescription};
use nym_node_requests::api::SignedHostInformation;
use nym_node_requests::routes;
use std::net::SocketAddr;
use std::path::Path;
@@ -34,14 +33,13 @@ pub struct HttpServerConfig {
}
impl HttpServerConfig {
pub fn new(host_information: SignedHostInformation) -> Self {
pub fn new() -> Self {
HttpServerConfig {
landing: Default::default(),
api: api::Config {
v1_config: api::v1::Config {
node: api::v1::node::Config {
build_information: bin_info_owned!(),
host_information,
system_info: None,
roles: Default::default(),
description: Default::default(),
@@ -118,6 +116,7 @@ impl HttpServerConfig {
self
}
#[must_use]
pub fn with_prometheus_bearer_token(mut self, bearer_token: Option<String>) -> Self {
self.api.v1_config.metrics.bearer_token = bearer_token.map(|b| Arc::new(Zeroizing::new(b)));
self
+23 -3
View File
@@ -1,20 +1,35 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2023-2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node::http::state::load::CachedNodeLoad;
use crate::node::http::state::metrics::MetricsAppState;
use crate::node::key_rotation::active_keys::ActiveSphinxKeys;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_node_metrics::NymNodeMetrics;
use nym_verloc::measurements::SharedVerlocStats;
use std::net::IpAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::time::Instant;
pub mod load;
pub mod metrics;
pub(crate) struct StaticNodeInformation {
pub(crate) ed25519_identity_keys: Arc<ed25519::KeyPair>,
pub(crate) x25519_noise_key: Option<x25519::PublicKey>,
pub(crate) ip_addresses: Vec<IpAddr>,
pub(crate) hostname: Option<String>,
}
#[derive(Clone)]
pub struct AppState {
pub(crate) struct AppState {
pub(crate) startup_time: Instant,
pub(crate) static_information: Arc<StaticNodeInformation>,
pub(crate) x25519_sphinx_keys: ActiveSphinxKeys,
pub(crate) cached_load: CachedNodeLoad,
pub(crate) metrics: MetricsAppState,
@@ -22,12 +37,17 @@ pub struct AppState {
impl AppState {
#[allow(clippy::new_without_default)]
pub fn new(
pub(crate) fn new(
static_information: StaticNodeInformation,
x25519_sphinx_keys: ActiveSphinxKeys,
metrics: NymNodeMetrics,
verloc: SharedVerlocStats,
load_cache_ttl: Duration,
) -> Self {
AppState {
static_information: Arc::new(static_information),
x25519_sphinx_keys,
// is it 100% accurate?
// no.
// does it have to be?