re-expose LP information on the http API

This commit is contained in:
Jędrzej Stuczyński
2026-02-25 16:06:07 +00:00
parent f2be036009
commit c10d97372f
23 changed files with 268 additions and 222 deletions
Generated
+1 -2
View File
@@ -7226,7 +7226,6 @@ dependencies = [
"opentelemetry_sdk",
"rand 0.8.5",
"rand 0.9.2",
"rand_chacha 0.3.1",
"serde",
"serde_json",
"sha2 0.10.9",
@@ -7274,9 +7273,9 @@ dependencies = [
"nym-http-api-client",
"nym-kkt-ciphersuite",
"nym-noise-keys",
"nym-test-utils",
"nym-upgrade-mode-check",
"nym-wireguard-types",
"rand_chacha 0.3.1",
"schemars 0.8.22",
"serde",
"serde_json",
@@ -20,7 +20,7 @@ use nym_api_requests::ecash::{
};
use nym_api_requests::models::{
ApiHealthResponse, GatewayCoreStatusResponse, HistoricalPerformanceResponse,
MixnodeCoreStatusResponse, NymNodeDescriptionV1,
MixnodeCoreStatusResponse, NymNodeDescriptionV1, NymNodeDescriptionV2,
};
use nym_api_requests::nym_nodes::{
NodesByAddressesResponse, SemiSkimmedNodesWithMetadata, SkimmedNode, SkimmedNodesWithMetadata,
@@ -273,18 +273,18 @@ impl<C, S> Client<C, S> {
Ok(history)
}
// #[deprecated(note = "use get_all_cached_described_nodes_v2 instead")]
#[deprecated(note = "use get_all_cached_described_nodes_v2 instead")]
pub async fn get_all_cached_described_nodes(
&self,
) -> Result<Vec<NymNodeDescriptionV1>, ValidatorClientError> {
Ok(self.nym_api.get_all_described_nodes().await?)
}
// pub async fn get_all_cached_described_nodes_v2(
// &self,
// ) -> Result<Vec<NymNodeDescriptionV2>, ValidatorClientError> {
// Ok(self.nym_api.get_all_described_nodes_v2().await?)
// }
pub async fn get_all_cached_described_nodes_v2(
&self,
) -> Result<Vec<NymNodeDescriptionV2>, ValidatorClientError> {
Ok(self.nym_api.get_all_described_nodes_v2().await?)
}
pub async fn get_all_cached_bonded_nym_nodes(
&self,
@@ -473,7 +473,7 @@ impl NymApiClient {
Ok(self.nym_api.health().await?)
}
// #[deprecated(note = "use .get_all_described_nodes_v2 instead")]
#[deprecated(note = "use .get_all_described_nodes_v2 instead")]
pub async fn get_all_described_nodes(
&self,
) -> Result<Vec<NymNodeDescriptionV1>, ValidatorClientError> {
@@ -495,29 +495,29 @@ impl NymApiClient {
Ok(descriptions)
}
// pub async fn get_all_described_nodes_v2(
// &self,
// ) -> Result<Vec<NymNodeDescriptionV2>, ValidatorClientError> {
// // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
// let mut page = 0;
// let mut descriptions = Vec::new();
//
// loop {
// let mut res = self
// .nym_api
// .get_nodes_described_v2(Some(page), None)
// .await?;
//
// descriptions.append(&mut res.data);
// if descriptions.len() < res.pagination.total {
// page += 1
// } else {
// break;
// }
// }
//
// Ok(descriptions)
// }
pub async fn get_all_described_nodes_v2(
&self,
) -> Result<Vec<NymNodeDescriptionV2>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut descriptions = Vec::new();
loop {
let mut res = self
.nym_api
.get_nodes_described_v2(Some(page), None)
.await?;
descriptions.append(&mut res.data);
if descriptions.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(descriptions)
}
pub async fn get_all_bonded_nym_nodes(
&self,
@@ -17,7 +17,7 @@ use nym_api_requests::ecash::VerificationKeyResponse;
use nym_api_requests::models::{
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody,
NymNodeDescriptionV1, PerformanceHistoryResponse, RewardedSetResponse,
NymNodeDescriptionV1, NymNodeDescriptionV2, PerformanceHistoryResponse, RewardedSetResponse,
SignerInformationResponse,
};
use nym_api_requests::nym_nodes::{
@@ -117,7 +117,7 @@ pub trait NymApiClientExt: ApiClient {
}
#[tracing::instrument(level = "debug", skip_all)]
// #[deprecated(note = "use .get_nodes_described_v2 instead")]
#[deprecated(note = "use .get_nodes_described_v2 instead")]
async fn get_nodes_described(
&self,
page: Option<u32>,
@@ -144,32 +144,32 @@ pub trait NymApiClientExt: ApiClient {
.await
}
// #[tracing::instrument(level = "debug", skip_all)]
// async fn get_nodes_described_v2(
// &self,
// page: Option<u32>,
// per_page: Option<u32>,
// ) -> Result<PaginatedResponse<NymNodeDescriptionV2>, NymAPIError> {
// let mut params = Vec::new();
//
// if let Some(page) = page {
// params.push(("page", page.to_string()))
// }
//
// if let Some(per_page) = per_page {
// params.push(("per_page", per_page.to_string()))
// }
//
// self.get_json(
// &[
// routes::V2_API_VERSION,
// routes::NYM_NODES_ROUTES,
// routes::NYM_NODES_DESCRIBED,
// ],
// &params,
// )
// .await
// }
#[tracing::instrument(level = "debug", skip_all)]
async fn get_nodes_described_v2(
&self,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedResponse<NymNodeDescriptionV2>, NymAPIError> {
let mut params = Vec::new();
if let Some(page) = page {
params.push(("page", page.to_string()))
}
if let Some(per_page) = per_page {
params.push(("per_page", per_page.to_string()))
}
self.get_json(
&[
routes::V2_API_VERSION,
routes::NYM_NODES_ROUTES,
routes::NYM_NODES_DESCRIBED,
],
&params,
)
.await
}
async fn get_current_rewarded_set(&self) -> Result<RewardedSetResponse, NymAPIError> {
self.get_rewarded_set().await
@@ -302,8 +302,8 @@ pub trait NymApiClientExt: ApiClient {
Ok(SkimmedNodesWithMetadata::new(nodes, metadata))
}
// #[deprecated(note = "use .get_all_described_nodes_v2 instead")]
// #[allow(deprecated)]
#[deprecated(note = "use .get_all_described_nodes_v2 instead")]
#[allow(deprecated)]
async fn get_all_described_nodes(&self) -> Result<Vec<NymNodeDescriptionV1>, NymAPIError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
@@ -323,24 +323,24 @@ pub trait NymApiClientExt: ApiClient {
Ok(descriptions)
}
// async fn (&self) -> Result<Vec<NymNodeDescriptionV2>, NymAPIError> {
// // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
// let mut page = 0;
// let mut descriptions = Vec::new();
//
// loop {
// let mut res = self.get_nodes_described_v2(Some(page), None).await?;
//
// descriptions.append(&mut res.data);
// if descriptions.len() < res.pagination.total {
// page += 1
// } else {
// break;
// }
// }
//
// Ok(descriptions)
// }
async fn get_all_described_nodes_v2(&self) -> Result<Vec<NymNodeDescriptionV2>, NymAPIError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut descriptions = Vec::new();
loop {
let mut res = self.get_nodes_described_v2(Some(page), None).await?;
descriptions.append(&mut res.data);
if descriptions.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(descriptions)
}
#[tracing::instrument(level = "debug", skip_all)]
async fn get_nym_nodes(
+1
View File
@@ -9,6 +9,7 @@ license.workspace = true
rust-version.workspace = true
readme.workspace = true
version.workspace = true
publish = false
[dependencies]
thiserror = { workspace = true }
+6 -7
View File
@@ -3,7 +3,7 @@
use crate::error::KKTCiphersuiteError;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use std::collections::HashMap;
use std::collections::BTreeMap;
use std::fmt::Display;
use strum_macros::{Display, EnumIter, EnumString};
@@ -47,12 +47,7 @@ pub mod xwing {
pub const PUBLIC_KEY_LENGTH: usize = x25519::PUBLIC_KEY_LENGTH + ml_kem768::PUBLIC_KEY_LENGTH;
}
pub type KEMKeyDigests = KeyDigests;
#[deprecated]
pub type SigningKeyDigests = KeyDigests;
pub type KeyDigests = HashMap<HashFunction, Vec<u8>>;
pub type KEMKeyDigests = BTreeMap<HashFunction, Vec<u8>>;
#[derive(
Clone,
@@ -66,6 +61,8 @@ pub type KeyDigests = HashMap<HashFunction, Vec<u8>>;
EnumIter,
EnumString,
Display,
Ord,
PartialOrd,
)]
#[strum(ascii_case_insensitive)]
#[strum(serialize_all = "lowercase")]
@@ -209,6 +206,8 @@ impl SignatureScheme {
EnumString,
Display,
Default,
Ord,
PartialOrd,
)]
#[strum(ascii_case_insensitive)]
#[strum(serialize_all = "lowercase")]
+1
View File
@@ -9,6 +9,7 @@ license.workspace = true
rust-version.workspace = true
readme.workspace = true
version.workspace = true
publish = false
[dependencies]
num_enum = { workspace = true }
+4 -4
View File
@@ -3,9 +3,9 @@
use libcrux_ml_kem::mlkem768::MlKem768KeyPair;
use libcrux_psq::handshake::types::DHKeyPair;
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, HashFunction, KeyDigests};
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, HashFunction, KEMKeyDigests};
use rand09::{CryptoRng, RngCore};
use std::collections::HashMap;
use std::collections::BTreeMap;
pub fn generate_lp_keypair_x25519<R>(rng: &mut R) -> DHKeyPair
where
@@ -38,9 +38,9 @@ pub fn hash_key_bytes(
/// attempt to produce digests of the provided key using all known [HashFunction] with a default
/// hash length where variable output is available
pub fn produce_key_digests(key_bytes: &[u8]) -> KeyDigests {
pub fn produce_key_digests(key_bytes: &[u8]) -> KEMKeyDigests {
use strum::IntoEnumIterator;
let mut digests = HashMap::new();
let mut digests = BTreeMap::new();
for hash in HashFunction::iter() {
digests.insert(hash, hash.digest(key_bytes, DEFAULT_HASH_LEN));
}
+15 -1
View File
@@ -3,10 +3,12 @@
use crate::error::KKTError;
use libcrux_psq::handshake::types::PQEncapsulationKey;
use nym_kkt_ciphersuite::KEM;
use nym_kkt_ciphersuite::{KEM, KEMKeyDigests};
use std::collections::BTreeMap;
use std::fmt::{Debug, Formatter};
use std::sync::Arc;
use crate::key_utils::produce_key_digests;
pub use libcrux_ml_kem::mlkem768::{MlKem768KeyPair, MlKem768PrivateKey, MlKem768PublicKey};
pub use libcrux_psq::classic_mceliece as mceliece;
pub use libcrux_psq::handshake::types::{DHKeyPair, DHPrivateKey, DHPublicKey};
@@ -41,6 +43,18 @@ impl KEMKeys {
}
}
pub fn encapsulation_keys_digests(&self) -> BTreeMap<KEM, KEMKeyDigests> {
let mut digests = BTreeMap::new();
let mlkem_digests = produce_key_digests(self.ml_kem768_pk.as_slice());
let mceliece_digests = produce_key_digests(self.mc_eliece_pk.as_ref().as_ref());
digests.insert(KEM::MlKem768, mlkem_digests);
digests.insert(KEM::McEliece, mceliece_digests);
digests
}
pub fn encoded_encapsulation_key(&self, kem: KEM) -> Option<&[u8]> {
match kem {
KEM::McEliece => Some(self.mc_eliece_pk.as_ref().as_ref()),
+1
View File
@@ -9,6 +9,7 @@ license.workspace = true
rust-version.workspace = true
readme.workspace = true
version.workspace = true
publish = false
[dependencies]
bytes = { workspace = true }
+8 -14
View File
@@ -4,7 +4,7 @@
use crate::LpError;
use nym_crypto::asymmetric::x25519;
use nym_kkt_ciphersuite::{Ciphersuite, KEM, KEMKeyDigests};
use std::collections::HashMap;
use std::collections::BTreeMap;
use std::fmt::Debug;
use std::sync::Arc;
@@ -46,17 +46,11 @@ impl LpLocalPeer {
/// Convert this `LpLocalPeer` into a valid `LpRemotePeer` that can be used within tests
#[doc(hidden)]
pub fn as_remote(&self) -> LpRemotePeer {
let mut expected_kem_key_digests = HashMap::new();
if let Some(keys) = &self.kem_keypairs {
for kem in [KEM::MlKem768, KEM::McEliece] {
expected_kem_key_digests.insert(
kem,
nym_kkt::key_utils::produce_key_digests(
keys.encoded_encapsulation_key(kem).unwrap(),
),
);
}
}
let expected_kem_key_digests = self
.kem_keypairs
.as_ref()
.map(|k| k.encapsulation_keys_digests())
.unwrap_or_default();
LpRemotePeer {
x25519_public: self.x25519.pk,
@@ -87,7 +81,7 @@ pub struct LpRemotePeer {
pub(crate) x25519_public: DHPublicKey,
/// Expected digests of the remote's KEM key
pub(crate) expected_kem_key_digests: HashMap<KEM, KEMKeyDigests>,
pub(crate) expected_kem_key_digests: BTreeMap<KEM, KEMKeyDigests>,
}
impl LpRemotePeer {
@@ -107,7 +101,7 @@ impl LpRemotePeer {
#[must_use]
pub fn with_key_digests(
mut self,
expected_kem_key_digests: HashMap<KEM, KEMKeyDigests>,
expected_kem_key_digests: BTreeMap<KEM, KEMKeyDigests>,
) -> Self {
self.expected_kem_key_digests = expected_kem_key_digests;
self
@@ -5,15 +5,16 @@
//! and defines required conversion methods
use celes::Country;
use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey;
use nym_crypto::asymmetric::ed25519::serde_helpers::{bs58_ed25519_pubkey, bs58_ed25519_signature};
use nym_crypto::asymmetric::x25519::serde_helpers::{bs58_dh_public_key, bs58_x25519_pubkey};
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_kkt_ciphersuite::{HashFunction, SignatureScheme, KEM};
use nym_network_defaults::{WG_METADATA_PORT, WG_TUNNEL_PORT};
use nym_node_requests::api::SignedData;
use nym_noise_keys::VersionedNoiseKeyV1;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::collections::BTreeMap;
use std::net::IpAddr;
use strum_macros::{Display, EnumString};
use thiserror::Error;
@@ -200,6 +201,16 @@ pub struct WireguardDetailsV1 {
// even if you put `#[serde(default)]` all over the place
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)]
pub struct LewesProtocolDetailsV1 {
pub content: LewesProtocolDetailsDataV1,
#[serde(with = "bs58_ed25519_signature")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub signature: ed25519::Signature,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)]
pub struct LewesProtocolDetailsDataV1 {
/// Helper field that specifies whether the LP listener(s) is enabled on this node.
/// It is directly controlled by the node's role (i.e. it is enabled if it supports 'entry' mode)
pub enabled: bool,
@@ -219,14 +230,14 @@ pub struct LewesProtocolDetailsV1 {
/// Digests of the KEM keys available to this node alongside hashing algorithms used
/// for their computation.
/// note: digests are hex encoded
pub kem_keys: HashMap<LPKEM, HashMap<LPHashFunction, String>>,
pub kem_keys: BTreeMap<LPKEM, BTreeMap<LPHashFunction, String>>,
}
impl LewesProtocolDetailsV1 {
impl LewesProtocolDetailsDataV1 {
fn decode_digests(
digests: &HashMap<LPHashFunction, String>,
) -> Result<HashMap<HashFunction, Vec<u8>>, MalformedLPData> {
let mut kem_digests = HashMap::new();
digests: &BTreeMap<LPHashFunction, String>,
) -> Result<BTreeMap<HashFunction, Vec<u8>>, MalformedLPData> {
let mut kem_digests = BTreeMap::new();
for (hash_function, digest) in digests {
let digest = hex::decode(digest).map_err(|source| MalformedLPData::MalformedHash {
value: digest.to_string(),
@@ -239,8 +250,8 @@ impl LewesProtocolDetailsV1 {
pub fn kem_keys(
&self,
) -> Result<HashMap<KEM, HashMap<HashFunction, Vec<u8>>>, MalformedLPData> {
let mut kem_keys = HashMap::new();
) -> Result<BTreeMap<KEM, BTreeMap<HashFunction, Vec<u8>>>, MalformedLPData> {
let mut kem_keys = BTreeMap::new();
for (kem, digests) in &self.kem_keys {
let kem_digests = Self::decode_digests(digests)?;
kem_keys.insert((*kem).try_into()?, kem_digests);
@@ -251,8 +262,8 @@ impl LewesProtocolDetailsV1 {
/// Convert map of digests from `nym_node_requests` types into `nym-api-requests` types
fn translate_digests(
digests: HashMap<nym_node_requests::api::v1::lewes_protocol::models::LPHashFunction, String>,
) -> HashMap<LPHashFunction, String> {
digests: BTreeMap<nym_node_requests::api::v1::lewes_protocol::models::LPHashFunction, String>,
) -> BTreeMap<LPHashFunction, String> {
digests
.into_iter()
.map(|(hash_fn, digest)| (hash_fn.into(), digest))
@@ -273,6 +284,7 @@ fn translate_digests(
Display,
EnumString,
ToSchema,
Ord,
)]
#[strum(serialize_all = "lowercase")]
#[non_exhaustive]
@@ -295,6 +307,7 @@ pub enum LPKEM {
Display,
EnumString,
ToSchema,
Ord,
)]
#[strum(serialize_all = "lowercase")]
#[non_exhaustive]
@@ -437,20 +450,26 @@ impl From<nym_node_requests::api::v1::gateway::models::Wireguard> for WireguardD
}
}
impl From<nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol>
impl From<SignedData<nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol>>
for LewesProtocolDetailsV1
{
fn from(value: nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol) -> Self {
fn from(
value: SignedData<nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol>,
) -> Self {
LewesProtocolDetailsV1 {
enabled: value.enabled,
control_port: value.control_port,
data_port: value.data_port,
x25519: value.x25519,
kem_keys: value
.kem_keys
.into_iter()
.map(|(kem, digests)| (kem.into(), translate_digests(digests)))
.collect(),
signature: value.signature,
content: LewesProtocolDetailsDataV1 {
enabled: value.enabled,
control_port: value.control_port,
data_port: value.data_port,
x25519: value.x25519,
kem_keys: value
.data
.kem_keys
.into_iter()
.map(|(kem, digests)| (kem.into(), translate_digests(digests)))
.collect(),
},
}
}
}
@@ -549,3 +568,14 @@ impl TryFrom<LPSignatureScheme> for SignatureScheme {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn signature_validity_after_conversion() {
// make sure the serialisation stays the same and signature is still valid
todo!()
}
}
+1 -1
View File
@@ -231,7 +231,7 @@ async fn get_bonded_nodes(
),
params(PaginationRequest)
)]
// #[deprecated(note = "use '/v2/nym-nodes/described' instead")]
#[deprecated(note = "use '/v2/nym-nodes/described' instead")]
async fn get_described_nodes(
State(state): State<AppState>,
Query(pagination): Query<PaginationRequest>,
+14 -18
View File
@@ -8,7 +8,7 @@ use axum::extract::{Query, State};
use axum::routing::get;
use axum::Router;
use nym_api_requests::models::NymNodeDescriptionV2;
use nym_api_requests::pagination::PaginatedResponse;
use nym_api_requests::pagination::{PaginatedResponse, Pagination};
use nym_http_api_common::FormattedResponse;
use tower_http::compression::CompressionLayer;
@@ -36,23 +36,19 @@ async fn get_described_nodes(
State(state): State<AppState>,
Query(pagination): Query<PaginationRequest>,
) -> AxumResult<FormattedResponse<PaginatedResponse<NymNodeDescriptionV2>>> {
let _ = state;
// TODO: implement it
let _ = pagination;
Err(AxumErrorResponse::not_implemented())
let output = pagination.output.unwrap_or_default();
// // TODO: implement it
// let _ = pagination;
// let output = pagination.output.unwrap_or_default();
//
// let cache = state.described_nodes_cache.get().await?;
// let descriptions = cache.all_nodes().cloned().collect::<Vec<_>>();
//
// Ok(output.to_response(PaginatedResponse {
// pagination: Pagination {
// total: descriptions.len(),
// page: 0,
// size: descriptions.len(),
// },
// data: descriptions,
// }))
let cache = state.described_nodes_cache.get().await?;
let descriptions = cache.all_nodes().cloned().collect::<Vec<_>>();
Ok(output.to_response(PaginatedResponse {
pagination: Pagination {
total: descriptions.len(),
page: 0,
size: descriptions.len(),
},
data: descriptions,
}))
}
-1
View File
@@ -134,7 +134,6 @@ cargo_metadata = { workspace = true }
[dev-dependencies]
criterion = { workspace = true, features = ["async_tokio"] }
rand_chacha = { workspace = true }
[features]
tokio-console = ["console-subscriber", "nym-task/tokio-tracing"]
+1 -1
View File
@@ -48,7 +48,7 @@ nym-bin-common = { workspace = true, features = [
[dev-dependencies]
tokio = { workspace = true, features = ["full"] }
rand_chacha = { workspace = true }
nym-test-utils = { workspace = true }
nym-crypto = { workspace = true, features = ["rand"] }
+2 -1
View File
@@ -12,6 +12,7 @@ use nym_http_api_client::{ApiClient, HttpClientError};
use super::v1::gateway::models::Wireguard;
use super::v1::metrics::models::SessionStats;
use crate::api::SignedLewesProtocol;
use crate::api::v1::authenticator::models::Authenticator;
use crate::api::v1::health::models::NodeHealth;
use crate::api::v1::ip_packet_router::models::IpPacketRouter;
@@ -98,7 +99,7 @@ pub trait NymNodeApiClientExt: ApiClient {
.await
}
async fn get_lewes_protocol(&self) -> Result<LewesProtocol, NymNodeApiClientError> {
async fn get_lewes_protocol(&self) -> Result<SignedLewesProtocol, NymNodeApiClientError> {
self.get_json_from(routes::api::v1::lp_absolute()).await
}
}
+19 -13
View File
@@ -20,6 +20,7 @@ pub use client::Client;
// create the type alias manually if openapi is not enabled
pub type SignedHostInformation = SignedData<crate::api::v1::node::models::HostInformation>;
pub type SignedLewesProtocol = SignedData<crate::api::v1::lewes_protocol::models::LewesProtocol>;
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct SignedDataHostInfo {
@@ -28,11 +29,20 @@ pub struct SignedDataHostInfo {
pub signature: String,
}
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct SignedLewesProtocolInfo {
// #[serde(flatten)]
pub data: crate::api::v1::lewes_protocol::models::LewesProtocol,
pub signature: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedData<T> {
// #[serde(flatten)]
pub data: T,
pub signature: String,
#[serde(with = "ed25519::bs58_ed25519_signature")]
pub signature: ed25519::Signature,
}
impl<T> SignedData<T> {
@@ -42,7 +52,7 @@ impl<T> SignedData<T> {
{
let plaintext = serde_json::to_string(&data)?;
let signature = key.sign(plaintext).to_base58_string();
let signature = key.sign(plaintext);
Ok(SignedData { data, signature })
}
@@ -54,11 +64,7 @@ impl<T> SignedData<T> {
return false;
};
let Ok(signature) = ed25519::Signature::from_base58_string(&self.signature) else {
return false;
};
key.verify(plaintext, &signature).is_ok()
key.verify(plaintext, &self.signature).is_ok()
}
}
@@ -72,7 +78,7 @@ impl SignedHostInformation {
let legacy_v3 = SignedData {
data: LegacyHostInformationV3::from(self.data.clone()),
signature: self.signature.clone(),
signature: self.signature,
};
if legacy_v3.verify(&self.keys.ed25519_identity) {
@@ -82,7 +88,7 @@ impl SignedHostInformation {
// attempt to verify legacy signatures
let legacy_v3 = SignedData {
data: LegacyHostInformationV3::from(self.data.clone()),
signature: self.signature.clone(),
signature: self.signature,
};
if legacy_v3.verify(&self.keys.ed25519_identity) {
@@ -91,7 +97,7 @@ impl SignedHostInformation {
let legacy_v2 = SignedData {
data: LegacyHostInformationV2::from(legacy_v3.data),
signature: self.signature.clone(),
signature: self.signature,
};
if legacy_v2.verify(&self.keys.ed25519_identity) {
@@ -100,7 +106,7 @@ impl SignedHostInformation {
SignedData {
data: LegacyHostInformationV1::from(legacy_v2.data),
signature: self.signature.clone(),
signature: self.signature,
}
.verify(&self.keys.ed25519_identity)
}
@@ -133,11 +139,11 @@ mod tests {
use crate::api::v1::node::models::{HostKeys, SphinxKey};
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_noise_keys::{NoiseVersion, VersionedNoiseKeyV1};
use rand_chacha::rand_core::SeedableRng;
use nym_test_utils::helpers::deterministic_rng;
#[test]
fn dummy_signed_host_verification() {
let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]);
let mut rng = deterministic_rng();
let ed22519 = ed25519::KeyPair::new(&mut rng);
let x25519_sphinx = x25519::KeyPair::new(&mut rng);
let x25519_sphinx2 = x25519::KeyPair::new(&mut rng);
@@ -5,7 +5,7 @@ use nym_crypto::asymmetric::x25519;
use nym_crypto::asymmetric::x25519::serde_helpers::bs58_dh_public_key;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::collections::BTreeMap;
use strum_macros::{Display, EnumIter, EnumString};
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
@@ -30,7 +30,7 @@ pub struct LewesProtocol {
/// Digests of the KEM keys available to this node alongside hashing algorithms used
/// for their computation.
/// note: digests are hex encoded
pub kem_keys: HashMap<LPKEM, HashMap<LPHashFunction, String>>,
pub kem_keys: BTreeMap<LPKEM, BTreeMap<LPHashFunction, String>>,
}
impl LewesProtocol {
@@ -39,7 +39,7 @@ impl LewesProtocol {
control_port: u16,
data_port: u16,
x25519: x25519::DHPublicKey,
kem_keys: HashMap<LPKEM, HashMap<LPHashFunction, String>>,
kem_keys: BTreeMap<LPKEM, BTreeMap<LPHashFunction, String>>,
) -> Self {
LewesProtocol {
enabled,
@@ -69,6 +69,7 @@ impl LewesProtocol {
PartialOrd,
Display,
EnumString,
Ord,
)]
#[strum(serialize_all = "lowercase")]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
@@ -109,6 +110,7 @@ impl From<nym_kkt_ciphersuite::KEM> for LPKEM {
Display,
EnumString,
EnumIter,
Ord,
)]
#[strum(serialize_all = "lowercase")]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
@@ -3,13 +3,13 @@
use axum::Router;
use axum::routing::get;
use nym_node_requests::api::v1::lewes_protocol::models;
use nym_node_requests::api::SignedLewesProtocol;
pub mod root;
#[derive(Debug, Default, Clone)]
#[derive(Debug, Clone)]
pub struct Config {
pub details: Option<models::LewesProtocol>,
pub details: SignedLewesProtocol,
}
pub(crate) fn routes<S: Send + Sync + 'static + Clone>(config: Config) -> Router<S> {
@@ -4,30 +4,29 @@
use axum::extract::Query;
use axum::http::StatusCode;
use nym_http_api_common::{FormattedResponse, OutputParams};
use nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol;
use nym_node_requests::api::{SignedLewesProtocol, SignedLewesProtocolInfo};
/// Returns root Lewes Protocol information
#[utoipa::path(
get,
path = "",
context_path = "/api/v1/lewes-protocol",
path = "/lewes-protocol",
context_path = "/api/v1",
tag = "Lewes Protocol",
responses(
(status = 501, description = "the endpoint hasn't been implemented yet"),
(status = 200, content(
(LewesProtocol = "application/json"),
(LewesProtocol = "application/yaml"),
(LewesProtocol = "application/bincode")
(SignedLewesProtocolInfo = "application/json"),
(SignedLewesProtocolInfo = "application/yaml"),
(SignedLewesProtocolInfo = "application/bincode")
))
),
params(OutputParams)
)]
pub(crate) async fn root_lewes_protocol(
config: Option<LewesProtocol>,
config: SignedLewesProtocol,
Query(output): Query<OutputParams>,
) -> Result<LewesProtocolResponse, StatusCode> {
let config = config.ok_or(StatusCode::NOT_IMPLEMENTED)?;
Ok(output.to_response(config))
}
pub type LewesProtocolResponse = FormattedResponse<LewesProtocol>;
pub type LewesProtocolResponse = FormattedResponse<SignedLewesProtocol>;
@@ -16,7 +16,8 @@ use nym_node_requests::api::{SignedDataHostInfo, v1::node::models::SignedHostInf
responses(
(status = 200, content(
(SignedDataHostInfo = "application/json"),
(SignedDataHostInfo = "application/yaml")
(SignedDataHostInfo = "application/yaml"),
(SignedDataHostInfo = "application/bincode")
))
),
params(OutputParams)
+6 -2
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::node::http::NymNodeHttpServer;
use crate::node::http::api::v1::lewes_protocol;
use crate::node::http::error::NymNodeHttpError;
use crate::node::http::state::AppState;
use axum::Router;
@@ -9,6 +10,7 @@ use axum::response::Redirect;
use axum::routing::get;
use nym_bin_common::bin_info_owned;
use nym_http_api_common::middleware::logging;
use nym_node_requests::api::SignedLewesProtocol;
use nym_node_requests::api::v1::authenticator::models::Authenticator;
use nym_node_requests::api::v1::gateway::models::{Bridges, Gateway};
use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter;
@@ -33,7 +35,7 @@ pub struct HttpServerConfig {
}
impl HttpServerConfig {
pub fn new() -> Self {
pub fn new(signed_lewes_protocol: SignedLewesProtocol) -> Self {
HttpServerConfig {
landing: Default::default(),
api: api::Config {
@@ -52,7 +54,9 @@ impl HttpServerConfig {
network_requester: Default::default(),
ip_packet_router: Default::default(),
authenticator: Default::default(),
lewes_protocol: Default::default(),
lewes_protocol: lewes_protocol::Config {
details: signed_lewes_protocol,
},
},
},
}
+39 -40
View File
@@ -57,6 +57,8 @@ use nym_network_requester::{
};
use nym_node_metrics::NymNodeMetrics;
use nym_node_metrics::events::MetricEventsSender;
use nym_node_requests::api::SignedData;
use nym_node_requests::api::v1::lewes_protocol::models::{LPHashFunction, LPKEM, LewesProtocol};
use nym_node_requests::api::v1::node::models::{AnnouncePorts, NodeDescription};
use nym_noise::config::{NoiseConfig, NoiseNetworkView};
use nym_noise_keys::VersionedNoiseKeyV1;
@@ -70,6 +72,7 @@ use nym_wireguard::{WireguardGatewayData, peer_controller::PeerControlRequest};
use rand::rngs::OsRng;
use rand::{CryptoRng, RngCore};
use rand09::SeedableRng;
use std::collections::BTreeMap;
use std::net::SocketAddr;
use std::ops::Deref;
use std::path::Path;
@@ -736,13 +739,11 @@ impl NymNode {
self.config.gateway_tasks.lp.control_bind_address,
self.config.gateway_tasks.lp.data_bind_address,
);
let lp_listener = gateway_tasks_builder
let mut lp_listener = gateway_tasks_builder
.build_lp_listener(wg_peer_registrator.clone(), active_clients_store.clone())
.await?;
// make sure lp listener can be built, but do not start it
let _ = lp_listener;
// self.shutdown_tracker()
// .try_spawn_named(async move { lp_listener.run().await }, "LpListener");
self.shutdown_tracker()
.try_spawn_named(async move { lp_listener.run().await }, "LpListener");
} else {
info!("node not running in entry mode: the websocket and LP will remain closed");
}
@@ -818,40 +819,24 @@ impl NymNode {
Ok(())
}
//
// fn compute_kem_key_hashes(&self) -> HashMap<LPKEM, HashMap<LPHashFunction, String>> {
// let kem = LPKEM::X25519;
//
// let kem_key_bytes = self.entry_gateway.psq_kem_key.public_key().as_bytes();
//
// // convert from `nym_kkt_ciphersuite` types into `nym_nodes_requests`
// let digests = produce_key_digests(kem_key_bytes.as_ref())
// .into_iter()
// .map(|(f, digest)| (f.into(), hex::encode(&digest)))
// .collect();
//
// let mut hashes = HashMap::new();
// hashes.insert(kem, digests);
// hashes
// }
//
// fn compute_signing_key_hashes(
// &self,
// ) -> HashMap<LPSignatureScheme, HashMap<LPHashFunction, String>> {
// let scheme = LPSignatureScheme::Ed25519;
//
// let kem_key_bytes = self.ed25519_identity_keys.public_key().as_bytes();
//
// // convert from `nym_kkt_ciphersuite` types into `nym_nodes_requests`
// let digests = produce_key_digests(kem_key_bytes.as_ref())
// .into_iter()
// .map(|(f, digest)| (f.into(), hex::encode(&digest)))
// .collect();
//
// let mut hashes = HashMap::new();
// hashes.insert(scheme, digests);
// hashes
// }
fn compute_kem_key_hashes(&self) -> BTreeMap<LPKEM, BTreeMap<LPHashFunction, String>> {
let digests = self.psq_kem_keys.encapsulation_keys_digests();
// convert from `nym_kkt_ciphersuite` types into `nym_nodes_requests`
digests
.into_iter()
.map(|(kem, kem_digests)| {
(
kem.into(),
kem_digests
.into_iter()
.map(|(f, digest)| (f.into(), hex::encode(&digest)))
.collect(),
)
})
.collect()
}
pub(crate) async fn build_http_server(&self) -> Result<NymNodeHttpServer, NymNodeError> {
let auxiliary_details = api_requests::v1::node::models::AuxiliaryDetails {
@@ -926,7 +911,21 @@ impl NymNode {
policy: None,
};
let mut config = HttpServerConfig::new()
let lewes_protocol = LewesProtocol {
enabled: self.modes().entry,
control_port: self.config.gateway_tasks.lp.announced_control_port(),
data_port: self.config.gateway_tasks.lp.announced_data_port(),
x25519: self.x25519_lp_keys.pk,
kem_keys: self.compute_kem_key_hashes(),
};
// SAFETY: the only way for this call to fail is if serialisation of LewesProtocol fails.
// however, that conversion is stable and infallible
#[allow(clippy::unwrap_used)]
let signed_lewes_protocol =
SignedData::new(lewes_protocol, self.ed25519_identity_keys.private_key()).unwrap();
let mut config = HttpServerConfig::new(signed_lewes_protocol)
.with_landing_page_assets(self.config.http.landing_page_assets_path.as_ref())
.with_mixnode_details(mixnode_details)
.with_gateway_details(gateway_details)