Compare commits

...

25 Commits

Author SHA1 Message Date
Georgio Nicolas a7587eafb7 make psq algos generic 2025-06-17 19:39:18 +02:00
Georgio Nicolas 6710e8df56 Add test which combines psq and kkt 2025-06-17 19:39:18 +02:00
Georgio Nicolas a5ba5e57ad Remove useless import 2025-06-17 19:39:18 +02:00
Georgio Nicolas 389d7d0b4e PSQ poc 2025-06-17 19:39:18 +02:00
Georgio Nicolas 650c034f46 Add copyright markers 2025-06-17 19:39:18 +02:00
Georgio Nicolas 9ca0ba802b Rename directory 2025-06-17 19:39:18 +02:00
Georgio Nicolas 118f836690 Add kkt protocol 2025-06-17 19:39:18 +02:00
Simon Wicky afbdc107bf backwards compatibility for mixnodes announced keys 2025-06-17 19:39:18 +02:00
benedetta davico a66852c0ae Bump ns-api version 2025-06-17 19:39:18 +02:00
Simon Wicky 04e4378352 add multiple output for semi-skimmed endpoint 2025-06-17 19:39:18 +02:00
Simon Wicky 2b056fdd6e change buffer allocation method and use connection timeout 2025-06-17 19:39:18 +02:00
Simon Wicky c20dcd7de9 restore start_send use 2025-06-17 19:39:18 +02:00
Simon Wicky 53caaf3ac7 improve noisestream creation and test noisepatterns 2025-06-17 19:39:18 +02:00
Simon Wicky 98c7b57468 add active_only option for semi-skimmed node build_response 2025-06-17 19:39:18 +02:00
Simon Wicky 77e1468aae fix asyncread and asyncwrite op following PR comment 2025-06-17 19:39:18 +02:00
Simon Wicky 00d509ca79 resolve non stream-related PR comments 2025-06-17 19:39:18 +02:00
Simon Wicky db5692ffc7 appease the clippy god 2025-06-17 19:39:18 +02:00
Simon Wicky 8151f65963 some comments and minor improvements for future versions 2025-06-17 19:39:18 +02:00
Jędrzej Stuczyński 8b24e426bf additional Polish; missing features, extra test, etc 2025-06-17 19:39:18 +02:00
Simon Wicky 507f40a963 noise handshake initiator side 2025-06-17 19:39:18 +02:00
Simon Wicky e06db2f18e noise handshake responder side 2025-06-17 19:39:18 +02:00
Simon Wicky 8b23552e77 noise handshake common 2025-06-17 19:39:18 +02:00
Simon Wicky 74b2aab446 add semi-skimmed endpoint to distribute noise key 2025-06-17 19:39:18 +02:00
Simon Wicky fb874d699b adding proper noise key announcing on nodes 2025-06-17 19:39:18 +02:00
Simon Wicky 764639971c add noise lib 2025-06-17 19:39:18 +02:00
27 changed files with 1749 additions and 368 deletions
Generated
+290 -32
View File
@@ -1457,6 +1457,16 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core-models"
version = "0.0.2"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"hax-lib",
"pastey",
"rand 0.9.1",
]
[[package]]
name = "cosmos-sdk-proto"
version = "0.26.1"
@@ -3141,6 +3151,40 @@ dependencies = [
"hashbrown 0.15.2",
]
[[package]]
name = "hax-lib"
version = "0.3.0"
source = "git+https://github.com/cryspen/hax/#27ba29ff727fb4c4fffecad5e736181087ce2537"
dependencies = [
"hax-lib-macros",
"num-bigint",
"num-traits",
]
[[package]]
name = "hax-lib-macros"
version = "0.3.0"
source = "git+https://github.com/cryspen/hax/#27ba29ff727fb4c4fffecad5e736181087ce2537"
dependencies = [
"hax-lib-macros-types",
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "hax-lib-macros-types"
version = "0.3.0"
source = "git+https://github.com/cryspen/hax/#27ba29ff727fb4c4fffecad5e736181087ce2537"
dependencies = [
"proc-macro2",
"quote",
"serde",
"serde_json",
"uuid",
]
[[package]]
name = "hdrhistogram"
version = "7.5.4"
@@ -3250,7 +3294,7 @@ dependencies = [
"idna",
"ipnet",
"once_cell",
"rand 0.9.0",
"rand 0.9.1",
"ring",
"rustls 0.23.25",
"thiserror 2.0.12",
@@ -3275,7 +3319,7 @@ dependencies = [
"moka",
"once_cell",
"parking_lot",
"rand 0.9.0",
"rand 0.9.1",
"resolv-conf",
"rustls 0.23.25",
"smallvec",
@@ -4150,6 +4194,194 @@ version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "libcrux-chacha20poly1305"
version = "0.0.2"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-hacl-rs",
"libcrux-macros",
"libcrux-poly1305",
]
[[package]]
name = "libcrux-curve25519"
version = "0.0.2"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-hacl-rs",
"libcrux-macros",
]
[[package]]
name = "libcrux-ecdh"
version = "0.0.3-alpha.1"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-curve25519",
"libcrux-p256",
"rand 0.9.1",
]
[[package]]
name = "libcrux-ed25519"
version = "0.0.3-alpha.1"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-hacl-rs",
"libcrux-macros",
"libcrux-sha2",
"rand_core 0.9.1",
]
[[package]]
name = "libcrux-hacl-rs"
version = "0.0.2"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-macros",
]
[[package]]
name = "libcrux-hkdf"
version = "0.0.3-alpha.1"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-hacl-rs",
"libcrux-hmac",
]
[[package]]
name = "libcrux-hmac"
version = "0.0.3-alpha.1"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-hacl-rs",
"libcrux-macros",
"libcrux-sha2",
]
[[package]]
name = "libcrux-intrinsics"
version = "0.0.2"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"core-models",
"hax-lib",
]
[[package]]
name = "libcrux-kem"
version = "0.0.3-alpha.1"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-ecdh",
"libcrux-ml-kem",
"libcrux-sha3",
"libcrux-traits",
"rand 0.9.1",
]
[[package]]
name = "libcrux-macros"
version = "0.0.2"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"quote",
"syn 2.0.98",
]
[[package]]
name = "libcrux-ml-kem"
version = "0.0.3-alpha.1"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"hax-lib",
"libcrux-intrinsics",
"libcrux-platform",
"libcrux-secrets",
"libcrux-sha3",
"rand 0.9.1",
]
[[package]]
name = "libcrux-p256"
version = "0.0.3-alpha.1"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-hacl-rs",
"libcrux-macros",
"libcrux-sha2",
]
[[package]]
name = "libcrux-platform"
version = "0.0.2"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libc",
]
[[package]]
name = "libcrux-poly1305"
version = "0.0.2"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-hacl-rs",
"libcrux-macros",
]
[[package]]
name = "libcrux-psq"
version = "0.0.3-alpha.1"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-chacha20poly1305",
"libcrux-ecdh",
"libcrux-ed25519",
"libcrux-hkdf",
"libcrux-hmac",
"libcrux-kem",
"libcrux-traits",
"rand 0.9.1",
]
[[package]]
name = "libcrux-secrets"
version = "0.0.2"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"hax-lib",
]
[[package]]
name = "libcrux-sha2"
version = "0.0.3-alpha.1"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"libcrux-hacl-rs",
"libcrux-macros",
"libcrux-traits",
]
[[package]]
name = "libcrux-sha3"
version = "0.0.2"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"hax-lib",
"libcrux-intrinsics",
"libcrux-platform",
]
[[package]]
name = "libcrux-traits"
version = "0.0.3-alpha.1"
source = "git+https://github.com/cryspen/libcrux#96abd9f2460ff92811a1c181662590d96c37949c"
dependencies = [
"rand 0.9.1",
]
[[package]]
name = "libm"
version = "0.2.11"
@@ -4892,18 +5124,18 @@ dependencies = [
"getset",
"hex",
"humantime-serde",
"nym-compact-ecash 0.1.0",
"nym-config 0.1.0",
"nym-contracts-common 0.5.0",
"nym-credentials-interface 0.1.0",
"nym-crypto 0.4.0",
"nym-ecash-time 0.1.0",
"nym-mixnet-contract-common 0.6.0",
"nym-network-defaults 0.1.0",
"nym-node-requests 0.1.0",
"nym-compact-ecash",
"nym-config",
"nym-contracts-common",
"nym-credentials-interface",
"nym-crypto",
"nym-ecash-time",
"nym-mixnet-contract-common",
"nym-network-defaults",
"nym-node-requests",
"nym-noise-keys",
"nym-serde-helpers 0.1.0",
"nym-ticketbooks-merkle 0.1.0",
"nym-serde-helpers",
"nym-ticketbooks-merkle",
"rand_chacha 0.3.1",
"schemars",
"serde",
@@ -6261,10 +6493,10 @@ version = "0.1.0"
dependencies = [
"dashmap",
"futures",
"nym-crypto 0.4.0",
"nym-crypto",
"nym-noise",
"nym-sphinx 0.1.0",
"nym-task 0.1.0",
"nym-sphinx",
"nym-task",
"rand 0.8.5",
"tokio",
"tokio-stream",
@@ -6530,7 +6762,7 @@ dependencies = [
"nym-mixnet-client",
"nym-network-requester",
"nym-node-metrics",
"nym-node-requests 0.1.0",
"nym-node-requests",
"nym-noise",
"nym-noise-keys",
"nym-nonexhaustive-delayqueue",
@@ -6593,12 +6825,12 @@ dependencies = [
"celes",
"humantime",
"humantime-serde",
"nym-bin-common 0.6.0",
"nym-crypto 0.4.0",
"nym-exit-policy 0.1.0",
"nym-http-api-client 0.1.0",
"nym-bin-common",
"nym-crypto",
"nym-exit-policy",
"nym-http-api-client",
"nym-noise-keys",
"nym-wireguard-types 0.1.0",
"nym-wireguard-types",
"rand_chacha 0.3.1",
"schemars",
"serde",
@@ -6777,14 +7009,12 @@ dependencies = [
name = "nym-noise"
version = "0.1.0"
dependencies = [
"anyhow",
"arc-swap",
"bytes",
"futures",
"nym-crypto 0.4.0",
"nym-crypto",
"nym-noise-keys",
"pin-project",
"rand_chacha 0.3.1",
"sha2 0.10.9",
"snow",
"strum 0.26.3",
@@ -6798,7 +7028,7 @@ dependencies = [
name = "nym-noise-keys"
version = "0.1.0"
dependencies = [
"nym-crypto 0.4.0",
"nym-crypto",
"schemars",
"serde",
"utoipa",
@@ -6906,6 +7136,29 @@ dependencies = [
"time",
]
[[package]]
name = "nym-psq-kkt"
version = "0.1.0"
dependencies = [
"arc-swap",
"bytes",
"criterion",
"futures",
"libcrux-kem",
"libcrux-psq",
"libcrux-traits",
"nym-crypto",
"pin-project",
"rand 0.9.1",
"sha2 0.10.9",
"snow",
"strum 0.26.3",
"thiserror 2.0.12",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "nym-sdk"
version = "0.1.0"
@@ -8286,6 +8539,12 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pastey"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a8cb46bdc156b1c90460339ae6bfd45ba0394e5effbaa640badb4987fdc261"
[[package]]
name = "peg"
version = "0.8.4"
@@ -8606,7 +8865,7 @@ dependencies = [
"hmac",
"md-5",
"memchr",
"rand 0.9.0",
"rand 0.9.1",
"sha2 0.10.9",
"stringprep",
]
@@ -8857,7 +9116,7 @@ checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc"
dependencies = [
"bytes",
"getrandom 0.3.1",
"rand 0.9.0",
"rand 0.9.1",
"ring",
"rustc-hash",
"rustls 0.23.25",
@@ -8911,13 +9170,12 @@ dependencies = [
[[package]]
name = "rand"
version = "0.9.0"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.1",
"zerocopy 0.8.20",
]
[[package]]
@@ -10982,7 +11240,7 @@ dependencies = [
"pin-project-lite",
"postgres-protocol",
"postgres-types",
"rand 0.9.0",
"rand 0.9.1",
"socket2",
"tokio",
"tokio-util",
+1
View File
@@ -64,6 +64,7 @@ members = [
"common/node-tester-utils",
"common/nonexhaustive-delayqueue",
"common/nym-id",
"common/nym-psq-kkt",
"common/nym-metrics",
"common/nym_offline_compact_ecash",
"common/nymnoise",
@@ -5,7 +5,7 @@ use dashmap::DashMap;
use futures::StreamExt;
use nym_noise::config::NoiseConfig;
use nym_noise::upgrade_noise_initiator;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_sphinx::addressing::nodes::NymNodeRoutingAddress;
use nym_sphinx::framing::codec::NymCodec;
use nym_sphinx::framing::packet::FramedNymPacket;
use std::io;
@@ -250,7 +250,7 @@ impl Client {
connections_count.fetch_add(1, Ordering::SeqCst);
ManagedConnection::new(
address,
address.into(),
noise_config,
receiver,
initial_connection_timeout,
@@ -25,9 +25,7 @@ use nym_api_requests::models::{
NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse,
};
use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated};
use nym_api_requests::nym_nodes::{
NodesByAddressesResponse, SemiSkimmedNodesWithMetadata, SkimmedNode, SkimmedNodesWithMetadata,
};
use nym_api_requests::nym_nodes::{NodesByAddressesResponse, SemiSkimmedNode, SkimmedNode};
use nym_coconut_dkg_common::types::EpochId;
use nym_http_api_client::UserAgent;
use nym_mixnet_contract_common::EpochRewardedSet;
@@ -567,6 +565,31 @@ impl NymApiClient {
Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata))
}
/// retrieve expanded information for all bonded nodes on the network
pub async fn get_all_expanded_nodes(
&self,
) -> Result<Vec<SemiSkimmedNode>, 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 nodes = Vec::new();
loop {
let mut res = self
.nym_api
.get_expanded_nodes(false, Some(page), None)
.await?;
nodes.append(&mut res.nodes.data);
if nodes.len() < res.nodes.pagination.total {
page += 1
} else {
break;
}
}
Ok(nodes)
}
pub async fn health(&self) -> Result<ApiHealthResponse, ValidatorClientError> {
Ok(self.nym_api.health().await?)
}
@@ -18,8 +18,8 @@ use nym_api_requests::models::{
NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse,
};
use nym_api_requests::nym_nodes::{
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponseV1,
PaginatedCachedNodesResponseV2,
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponse,
SemiSkimmedNode,
};
use nym_api_requests::pagination::PaginatedResponse;
pub use nym_api_requests::{
@@ -676,6 +676,39 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[instrument(level = "debug", skip(self))]
async fn get_expanded_nodes(
&self,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedCachedNodesResponse<SemiSkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if no_legacy {
params.push(("no_legacy", "true".to_string()))
}
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::API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"semi-skimmed",
],
&params,
)
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_active_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
+37
View File
@@ -0,0 +1,37 @@
[package]
name = "nym-psq-kkt"
version = "0.1.0"
authors = ["Georgio Nicolas <georgio@nymtech.net>"]
edition = "2021"
license.workspace = true
[dependencies]
arc-swap = { workspace = true }
bytes = { workspace = true }
futures = { workspace = true }
tracing = { workspace = true }
pin-project = { workspace = true }
sha2 = { workspace = true }
snow = { workspace = true }
strum = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["full"] }
tokio-util = { workspace = true, features = ["codec"] }
# internal
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde"]}
libcrux-traits = { git = "https://github.com/cryspen/libcrux" }
libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = ["test-utils"] }
rand = "0.9.1"
[dev-dependencies]
criterion = {workspace = true}
[[bench]]
name = "benches"
harness = false
[lints]
workspace = true
+135
View File
@@ -0,0 +1,135 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use criterion::{criterion_group, criterion_main, Criterion};
use libcrux_psq::impls::MlKem768;
use nym_crypto::asymmetric::ed25519;
use nym_psq_kkt::kkt::{
KKTInitiator, KKTResponder, KKT_REQ_LEN, KKT_RES_LEN_MLKEM768, KKT_TAG_LEN,
};
use rand::prelude::*;
pub fn gen_ed25519_keypair(c: &mut Criterion) {
c.bench_function("Generate Ed25519 Keypair", |b| {
b.iter(|| {
let mut s: [u8; 32] = [0u8; 32];
rand::rng().fill_bytes(&mut s);
ed25519::KeyPair::from_secret(s, 0)
});
});
}
pub fn gen_mlkem768_keypair(c: &mut Criterion) {
c.bench_function("Generate MlKem768 Keypair", |b| {
b.iter(|| {
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand::rng()).unwrap()
});
});
}
pub fn kkt_benchmark(c: &mut Criterion) {
// generate ed25519 keys
let mut secret_initiator: [u8; 32] = [0u8; 32];
rand::rng().fill_bytes(&mut secret_initiator);
let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0);
let mut secret_responder: [u8; 32] = [0u8; 32];
rand::rng().fill_bytes(&mut secret_responder);
let responder_ed25519_keypair = ed25519::KeyPair::from_secret(secret_responder, 1);
// generate kem keypair
let (_, responder_kem_public_key) =
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand::rng()).unwrap();
// initialize parties
let initiator: KKTInitiator<MlKem768> =
KKTInitiator::init(initiator_ed25519_keypair.private_key());
c.bench_function("Initialize Initiator", |b| {
b.iter(|| KKTInitiator::<MlKem768>::init(initiator_ed25519_keypair.private_key()));
});
let responder: KKTResponder<MlKem768> = KKTResponder::init(
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
);
c.bench_function("Initialize Responder", |b| {
b.iter(|| {
KKTResponder::init(
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
)
});
});
// create buffers
let mut request_buffer: [u8; KKT_REQ_LEN] = [0u8; KKT_REQ_LEN];
let mut response_buffer: [u8; KKT_RES_LEN_MLKEM768] = [0u8; KKT_RES_LEN_MLKEM768];
let mut tag_buffer: [u8; KKT_TAG_LEN] = [0u8; KKT_TAG_LEN];
c.bench_function("Initiator: Generate Request", |b| {
b.iter(|| {
initiator.request_kem_pk(&mut request_buffer, &mut tag_buffer);
});
});
// generate request
initiator.request_kem_pk(&mut request_buffer, &mut tag_buffer);
c.bench_function("Responder: Ingest Request and Generate Response", |b| {
b.iter(|| {
responder
.respond_kem_pk(
&mut response_buffer,
initiator_ed25519_keypair.public_key(),
&request_buffer,
)
.unwrap();
});
});
// ingest request, generate response
responder
.respond_kem_pk(
&mut response_buffer,
initiator_ed25519_keypair.public_key(),
&request_buffer,
)
.unwrap();
c.bench_function("Initiator: Ingest Response and Store Key", |b| {
b.iter(|| {
let _ = initiator
.ingest_response_kem_pk::<MlKem768>(
&response_buffer,
&tag_buffer,
responder_ed25519_keypair.public_key(),
)
.unwrap();
});
});
// ingest response
let received_responder_key = initiator
.ingest_response_kem_pk::<MlKem768>(
&response_buffer,
&tag_buffer,
responder_ed25519_keypair.public_key(),
)
.unwrap();
// check if the public key received is the same one that we generated at the start
assert_eq!(
responder_kem_public_key.encode(),
received_responder_key.encode()
);
}
criterion_group!(
benches,
gen_ed25519_keypair,
gen_mlkem768_keypair,
kkt_benchmark
);
criterion_main!(benches);
+66
View File
@@ -0,0 +1,66 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::io;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum PSQError {
#[error("encountered a decryption error")]
DecryptionError,
#[error("encountered a KEM error")]
KEMError,
#[error("encountered a PSQ error")]
PSQError,
#[error("encountered an IO error: {0}")]
IoError(#[from] io::Error),
#[error("Incorrect state")]
IncorrectStateError,
#[error("Handshake did not complete")]
HandshakeError,
#[error("Unknown noise version")]
UnknownVersion,
#[error("Handshake timeout")]
HandshakeTimeout(#[from] tokio::time::error::Elapsed),
}
impl From<libcrux_kem::Error> for PSQError {
fn from(err: libcrux_kem::Error) -> Self {
match err {
// Error::Decrypt => PSQError::DecryptionError,
err => PSQError::KEMError,
}
}
}
impl From<libcrux_psq::Error> for PSQError {
fn from(err: libcrux_psq::Error) -> Self {
match err {
// Error::Decrypt => PSQError::DecryptionError,
err => PSQError::PSQError,
}
}
}
#[derive(Error, Debug)]
pub enum KKTError {
#[error("Signature constructor error")]
SigConstructorError,
#[error("Signature verification error")]
SigVerifError,
// #[error("Protocol did not complete")]
// ProtocolError,
// #[error("encountered an IO error: {0}")]
// IoError(#[from] io::Error),
// #[error("Handshake timeout")]
// HandshakeTimeout(#[from] tokio::time::error::Elapsed),
}
+227
View File
@@ -0,0 +1,227 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::marker::PhantomData;
use nym_crypto::asymmetric::ed25519;
use rand::prelude::*;
use libcrux_kem::MlKem768PublicKey;
use libcrux_psq::{impls::MlKem768, traits::PSQ};
use libcrux_traits::kem::KEM;
use crate::error::KKTError;
const REQ_STR: &[u8] = "KEM_REQ".as_bytes();
const RES_STR: &[u8] = "KEM_RES".as_bytes();
const REQ_LEN: usize = REQ_STR.len() + KKT_TAG_LEN;
const RES_LEN_MLKEM768: usize = REQ_LEN + MlKem768PublicKey::len();
pub const KKT_TAG_LEN: usize = 16;
pub const KKT_REQ_LEN: usize = REQ_LEN + ed25519::SIGNATURE_LENGTH;
pub const KKT_RES_LEN_MLKEM768: usize = RES_LEN_MLKEM768 + ed25519::SIGNATURE_LENGTH;
pub struct KKTInitiator<'a, T: PSQ> {
signing_key: &'a ed25519::PrivateKey,
_t: PhantomData<T>,
}
impl<'a, T: PSQ> KKTInitiator<'a, T> {
pub fn init(signing_key: &'a ed25519::PrivateKey) -> Self {
Self {
signing_key,
_t: PhantomData,
}
}
pub fn request_kem_pk(
&self,
request_buffer: &mut [u8; KKT_REQ_LEN],
tag_buffer: &mut [u8; KKT_TAG_LEN],
) {
// request_buffer[0..7] <- request string
request_buffer[0..REQ_STR.len()].copy_from_slice(REQ_STR);
// tag_buffer <- generate tag
rand::rng().fill_bytes(tag_buffer);
// request_buffer[7..23] <- tag
request_buffer[REQ_STR.len()..REQ_LEN].copy_from_slice(tag_buffer);
// sig <- Sign request_buffer[0..23]
let sig = self.signing_key.sign(&request_buffer[0..REQ_LEN]);
// request_buffer[23..87] <- sig
request_buffer[REQ_LEN..].copy_from_slice(&sig.to_bytes());
}
}
impl<'a> KKTInitiator<'a, MlKem768> {
pub fn ingest_response_kem_pk<T: PSQ>(
&self,
response: &[u8],
tag: &[u8; KKT_TAG_LEN],
responder_verification_key: &ed25519::PublicKey,
) -> Result<libcrux_kem::PublicKey, KKTError> {
// TODO: Refactor asserts into errors
// Check size of message
assert_eq!(response.len(), KKT_RES_LEN_MLKEM768);
// Check if the response string is there at the start of the message
assert_eq!(&response[0..RES_STR.len()], RES_STR);
// Check if the tag is the one we expect
assert_eq!(
&response[RES_STR.len()..RES_STR.len() + KKT_TAG_LEN],
&tag[..]
);
// Attempt to reconstruct a signature from the received bytes
match ed25519::Signature::from_bytes(&response[RES_LEN_MLKEM768..]) {
Ok(sig) => {
// Attempt to verify signature
match responder_verification_key.verify(&response[0..RES_LEN_MLKEM768], &sig) {
Ok(()) => {
// Extract key from bytes
// (has to be an owned sized array, unless I'm missing some function that works with slices)
let mut key_bytes: [u8; MlKem768PublicKey::len()] =
[0u8; MlKem768PublicKey::len()];
key_bytes.copy_from_slice(
&response[RES_STR.len() + KKT_TAG_LEN..RES_LEN_MLKEM768],
);
// Create key from bytes and return it
Ok(libcrux_kem::PublicKey::MlKem768(MlKem768PublicKey::from(
key_bytes,
)))
}
Err(_) => Err(KKTError::SigVerifError),
}
}
Err(_) => Err(KKTError::SigConstructorError),
}
}
}
pub struct KKTResponder<'a, T: PSQ> {
signing_key: &'a ed25519::PrivateKey,
kem_public_key: &'a <T::InnerKEM as KEM>::EncapsulationKey,
}
impl<'a> KKTResponder<'a, MlKem768> {
pub fn init(
signing_key: &'a ed25519::PrivateKey,
kem_public_key: &'a libcrux_kem::PublicKey,
) -> Self {
Self {
signing_key,
kem_public_key,
}
}
pub fn respond_kem_pk(
&self,
response_buffer: &mut [u8; KKT_RES_LEN_MLKEM768],
initiator_verification_key: &ed25519::PublicKey,
request: &[u8],
) -> Result<(), KKTError> {
// TODO: Refactor asserts into errors
// Check request size
assert_eq!(request.len(), KKT_REQ_LEN);
// Check request string
assert_eq!(&request[0..REQ_STR.len()], REQ_STR);
match ed25519::Signature::from_bytes(&request[REQ_LEN..]) {
Ok(sig) => match initiator_verification_key.verify(&request[0..REQ_LEN], &sig) {
Ok(()) => {
// response_buffer[0..7] <- RES_STR (7 bytes)
response_buffer[0..RES_STR.len()].copy_from_slice(RES_STR);
// response_buffer[7..23] <- tag (16 bytes, sent by initiator)
response_buffer[RES_STR.len()..REQ_LEN]
.copy_from_slice(&request[REQ_STR.len()..REQ_LEN]);
// response_buffer[23..1207] <- MlKem768 Public Key (1184 bytes)
response_buffer[REQ_LEN..RES_LEN_MLKEM768]
.copy_from_slice(self.kem_public_key.encode().as_slice());
// sign response_buffer[0..1207]
let sig = self.signing_key.sign(&response_buffer[0..RES_LEN_MLKEM768]);
// response_buffer[1207..1271] <- signature (64 bytes)
response_buffer[RES_LEN_MLKEM768..].copy_from_slice(&sig.to_bytes());
Ok(())
}
Err(_) => Err(KKTError::SigConstructorError),
},
Err(_) => Err(KKTError::SigVerifError),
}
}
}
#[cfg(test)]
mod test {
use crate::kkt::{KKT_REQ_LEN, KKT_RES_LEN_MLKEM768, KKT_TAG_LEN};
use super::{KKTInitiator, KKTResponder};
use libcrux_psq::impls::MlKem768;
use nym_crypto::asymmetric::ed25519;
use rand::prelude::*;
#[test]
fn test_kkt_e2e() {
// generate ed25519 keys
let mut secret_initiator: [u8; 32] = [0u8; 32];
rand::rng().fill_bytes(&mut secret_initiator);
let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0);
let mut secret_responder: [u8; 32] = [0u8; 32];
rand::rng().fill_bytes(&mut secret_responder);
let responder_ed25519_keypair = ed25519::KeyPair::from_secret(secret_responder, 1);
// generate kem keypair
let (_, responder_kem_public_key) =
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand::rng()).unwrap();
// initialize parties
let initiator: KKTInitiator<MlKem768> =
KKTInitiator::init(initiator_ed25519_keypair.private_key());
let responder: KKTResponder<MlKem768> = KKTResponder::init(
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
);
// create buffers
let mut request_buffer: [u8; KKT_REQ_LEN] = [0u8; KKT_REQ_LEN];
let mut response_buffer: [u8; KKT_RES_LEN_MLKEM768] = [0u8; KKT_RES_LEN_MLKEM768];
let mut tag_buffer: [u8; KKT_TAG_LEN] = [0u8; KKT_TAG_LEN];
// generate request
initiator.request_kem_pk(&mut request_buffer, &mut tag_buffer);
// ingest request, generate response
responder
.respond_kem_pk(
&mut response_buffer,
initiator_ed25519_keypair.public_key(),
&request_buffer,
)
.unwrap();
// ingest response
let received_responder_key = initiator
.ingest_response_kem_pk::<MlKem768>(
&response_buffer,
&tag_buffer,
responder_ed25519_keypair.public_key(),
)
.unwrap();
// check if the public key received is the same one that we generated at the start
assert_eq!(
responder_kem_public_key.encode(),
received_responder_key.encode()
);
}
}
+94
View File
@@ -0,0 +1,94 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod error;
pub mod kkt;
pub mod psq;
#[cfg(test)]
mod test {
use libcrux_psq::impls::MlKem768;
use nym_crypto::asymmetric::ed25519;
use rand::prelude::*;
use crate::{
kkt::{KKTInitiator, KKTResponder, KKT_REQ_LEN, KKT_RES_LEN_MLKEM768, KKT_TAG_LEN},
psq::{PSQInitiator, PSQResponder},
};
#[test]
fn test_kkt_psq_e2e() {
let mut rng = rand::rng();
// generate ed25519 keys
let mut secret_initiator: [u8; 32] = [0u8; 32];
rng.fill_bytes(&mut secret_initiator);
let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0);
let mut secret_responder: [u8; 32] = [0u8; 32];
rng.fill_bytes(&mut secret_responder);
let responder_ed25519_keypair = ed25519::KeyPair::from_secret(secret_responder, 1);
// generate kem keypair
let (responder_kem_private_key, responder_kem_public_key) =
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rng).unwrap();
// initialize parties
let kkt_initiator: KKTInitiator<MlKem768> =
KKTInitiator::init(initiator_ed25519_keypair.private_key());
let kkt_responder: KKTResponder<MlKem768> = KKTResponder::init(
responder_ed25519_keypair.private_key(),
&responder_kem_public_key,
);
// create buffers
let mut request_buffer: [u8; KKT_REQ_LEN] = [0u8; KKT_REQ_LEN];
let mut response_buffer: [u8; KKT_RES_LEN_MLKEM768] = [0u8; KKT_RES_LEN_MLKEM768];
let mut tag_buffer: [u8; KKT_TAG_LEN] = [0u8; KKT_TAG_LEN];
// generate request
kkt_initiator.request_kem_pk(&mut request_buffer, &mut tag_buffer);
// ingest request, generate response
kkt_responder
.respond_kem_pk(
&mut response_buffer,
initiator_ed25519_keypair.public_key(),
&request_buffer,
)
.unwrap();
// ingest response
let received_responder_key = kkt_initiator
.ingest_response_kem_pk::<MlKem768>(
&response_buffer,
&tag_buffer,
responder_ed25519_keypair.public_key(),
)
.unwrap();
// check if the public key received is the same one that we generated at the start
assert_eq!(
responder_kem_public_key.encode(),
received_responder_key.encode()
);
let mut psq_initiator: PSQInitiator<MlKem768> =
PSQInitiator::init(&initiator_ed25519_keypair);
let psq_responder: PSQResponder<MlKem768> =
PSQResponder::init(&responder_kem_private_key, &responder_kem_public_key);
let initiator_psq_msg = psq_initiator
.initiator_message(&mut rng, &received_responder_key)
.unwrap();
let (responder_psk, responder_psq_msg) = psq_responder
.responder_msg(initiator_ed25519_keypair.public_key(), &initiator_psq_msg)
.unwrap();
let initiator_psk = psq_initiator.finalize(&responder_psq_msg).unwrap();
assert_eq!(initiator_psk, responder_psk);
}
}
+182
View File
@@ -0,0 +1,182 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::{marker::PhantomData, time::Duration};
use libcrux_psq::{
cred::Ed25519,
psk_registration::{Initiator, InitiatorMsg, Responder, ResponderMsg},
traits::PSQ,
};
use libcrux_traits::kem::KEM;
use nym_crypto::asymmetric::ed25519;
use rand::CryptoRng;
use crate::error::PSQError;
pub const PSK_LENGTH: usize = 32;
pub struct PSQInitiator<'a, T: PSQ> {
signing_keypair: &'a ed25519::KeyPair,
state: Option<Initiator>,
_t: PhantomData<T>,
}
impl<'a, T: PSQ> PSQInitiator<'a, T> {
pub fn init(signing_keypair: &'a ed25519::KeyPair) -> Self {
Self {
signing_keypair,
state: None,
_t: PhantomData,
}
}
pub fn initiator_message(
&mut self,
rng: &mut impl CryptoRng,
responder_kem_public_key: &'a <T::InnerKEM as KEM>::EncapsulationKey,
) -> Result<InitiatorMsg<T::InnerKEM>, PSQError> {
let (state, message) = Initiator::send_initial_message::<Ed25519, T>(
&[0u8],
Duration::from_secs(3600),
&responder_kem_public_key,
&self.signing_keypair.private_key().to_bytes(),
&self.signing_keypair.public_key().to_bytes(),
rng,
)?;
self.state = Some(state);
Ok(message)
}
pub fn finalize(&self, responder_message: &ResponderMsg) -> Result<[u8; PSK_LENGTH], PSQError> {
match &self.state {
Some(state) => match state.complete_handshake(responder_message) {
Ok(psk) => Ok(psk.psk),
Err(err) => Err(PSQError::KEMError),
},
None => Err(PSQError::IncorrectStateError),
}
}
}
pub struct PSQResponder<'a, T: PSQ> {
kem_private_key: &'a <T::InnerKEM as KEM>::DecapsulationKey,
kem_public_key: &'a <T::InnerKEM as KEM>::EncapsulationKey,
_t: PhantomData<T>,
}
impl<'a, T: PSQ> PSQResponder<'a, T> {
pub fn init(
kem_private_key: &'a <T::InnerKEM as KEM>::DecapsulationKey,
kem_public_key: &'a <T::InnerKEM as KEM>::EncapsulationKey,
) -> Self {
Self {
kem_private_key,
kem_public_key,
_t: PhantomData,
}
}
pub fn responder_msg(
&self,
initiator_verification_key: &'a ed25519::PublicKey,
initiator_message: &InitiatorMsg<T::InnerKEM>,
) -> Result<([u8; PSK_LENGTH], ResponderMsg), PSQError> {
let (registered_psk, responder_msg) = Responder::send::<Ed25519, T>(
&[0u8],
Duration::from_secs(3600),
&[0u8],
self.kem_public_key,
self.kem_private_key,
&initiator_verification_key.to_bytes(),
initiator_message,
)?;
Ok((registered_psk.psk, responder_msg))
}
}
#[cfg(test)]
mod test {
use libcrux_psq::impls::{MlKem768, XWingKemDraft06, X25519};
use libcrux_psq::traits::PSQ;
use libcrux_traits::kem::KEM;
use nym_crypto::asymmetric::ed25519;
use rand::prelude::*;
use super::{PSQInitiator, PSQResponder};
fn test_helper<T: PSQ>(
rng: &mut impl CryptoRng,
responder_kem_private_key: <T::InnerKEM as KEM>::DecapsulationKey,
responder_kem_public_key: <T::InnerKEM as KEM>::EncapsulationKey,
) {
// generate ed25519 keys
let mut secret_initiator: [u8; 32] = [0u8; 32];
rng.fill_bytes(&mut secret_initiator);
let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0);
let mut initiator: PSQInitiator<T> = PSQInitiator::init(&initiator_ed25519_keypair);
let responder: PSQResponder<T> =
PSQResponder::init(&responder_kem_private_key, &responder_kem_public_key);
let initiator_msg = initiator
.initiator_message(rng, &responder_kem_public_key)
.unwrap();
let (responder_psk, responder_msg) = responder
.responder_msg(initiator_ed25519_keypair.public_key(), &initiator_msg)
.unwrap();
let initiator_psk = initiator.finalize(&responder_msg).unwrap();
assert_eq!(initiator_psk, responder_psk);
}
#[test]
fn test_psq_e2e_mlkem() {
let mut rng = rand::rng();
// generate mlkem keypair
let (responder_kem_private_key, responder_kem_public_key) =
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rng).unwrap();
test_helper::<MlKem768>(
&mut rng,
responder_kem_private_key,
responder_kem_public_key,
);
}
#[test]
fn test_psq_e2e_xwing() {
let mut rng = rand::rng();
// generate mlkem keypair
let (responder_kem_private_key, responder_kem_public_key) =
libcrux_kem::key_gen(libcrux_kem::Algorithm::XWingKemDraft06, &mut rand::rng())
.unwrap();
test_helper::<XWingKemDraft06>(
&mut rng,
responder_kem_private_key,
responder_kem_public_key,
);
}
#[test]
fn test_psq_e2e_dhkem() {
let mut rng = rand::rng();
// generate mlkem keypair
let (responder_kem_private_key, responder_kem_public_key) =
libcrux_kem::key_gen(libcrux_kem::Algorithm::X25519, &mut rand::rng()).unwrap();
test_helper::<X25519>(
&mut rng,
responder_kem_private_key,
responder_kem_public_key,
);
}
}
-7
View File
@@ -22,12 +22,5 @@ tokio-util = { workspace = true, features = ["codec"] }
nym-crypto = { path = "../crypto" }
nym-noise-keys = { path = "keys" }
[dev-dependencies]
anyhow = { workspace = true }
tokio = { workspace = true, features = ["full"] }
rand_chacha = { workspace = true }
nym-crypto = { path = "../crypto", features = ["rand"] }
[lints]
workspace = true
+5 -8
View File
@@ -8,25 +8,22 @@ use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(from = "u8", into = "u8")]
pub enum NoiseVersion {
V1,
Unknown(u8), //Implies a newer version we don't know
V1 = 1,
Unknown, //Implies a newer version we don't know
}
impl From<u8> for NoiseVersion {
fn from(value: u8) -> Self {
match value {
1 => NoiseVersion::V1,
other => NoiseVersion::Unknown(other),
_ => NoiseVersion::Unknown,
}
}
}
impl From<NoiseVersion> for u8 {
fn from(version: NoiseVersion) -> Self {
match version {
NoiseVersion::V1 => 1,
NoiseVersion::Unknown(other) => other,
}
version as u8
}
}
@@ -34,7 +31,7 @@ impl From<NoiseVersion> for u8 {
pub struct VersionedNoiseKey {
#[schemars(with = "u8")]
#[schema(value_type = u8)]
pub supported_version: NoiseVersion,
pub version: NoiseVersion,
#[schemars(with = "String")]
#[serde(with = "bs58_x25519_pubkey")]
+6 -8
View File
@@ -13,19 +13,17 @@ use nym_crypto::asymmetric::x25519;
use nym_noise_keys::{NoiseVersion, VersionedNoiseKey};
use snow::params::NoiseParams;
use strum::{EnumIter, FromRepr};
use strum::EnumIter;
#[derive(Default, Debug, Clone, Copy, EnumIter, FromRepr, Eq, PartialEq)]
#[repr(u8)]
#[non_exhaustive]
#[derive(Default, Debug, Clone, Copy, EnumIter)]
pub enum NoisePattern {
#[default]
XKpsk3 = 1,
IKpsk2 = 2,
XKpsk3,
IKpsk2,
}
impl NoisePattern {
pub(crate) const fn as_str(&self) -> &'static str {
pub(crate) fn as_str(&self) -> &'static str {
match self {
Self::XKpsk3 => "Noise_XKpsk3_25519_AESGCM_SHA256",
Self::IKpsk2 => "Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s", //Wireguard handshake (not exactly though)
@@ -81,7 +79,7 @@ impl NoiseNetworkView {
pub fn swap_view(&self, new: HashMap<SocketAddr, VersionedNoiseKey>) {
let noise_support = new
.iter()
.map(|(s_addr, key)| (s_addr.ip(), key.supported_version))
.map(|(s_addr, key)| (s_addr.ip(), key.version))
.collect::<HashMap<_, _>>();
self.keys.inner.store(Arc::new(new));
self.support.inner.store(Arc::new(noise_support));
+22 -16
View File
@@ -4,21 +4,30 @@
use std::io;
use pin_project::pin_project;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::{
io::{AsyncRead, AsyncWrite},
net::TcpStream,
};
use crate::stream::NoiseStream;
//SW once plain TCP support is dropped, this whole enum can be dropped, and we can only propagate NoiseStream
#[pin_project(project = ConnectionProj)]
pub enum Connection<C> {
Raw(#[pin] C),
Noise(#[pin] Box<NoiseStream<C>>),
pub enum Connection {
Tcp(#[pin] TcpStream),
Noise(#[pin] NoiseStream),
}
impl<C> AsyncRead for Connection<C>
where
C: AsyncRead + AsyncWrite + Unpin,
{
impl Connection {
pub fn peer_addr(&self) -> Result<std::net::SocketAddr, io::Error> {
match self {
Self::Noise(stream) => stream.peer_addr(),
Self::Tcp(stream) => stream.peer_addr(),
}
}
}
impl AsyncRead for Connection {
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
@@ -26,15 +35,12 @@ where
) -> std::task::Poll<io::Result<()>> {
match self.project() {
ConnectionProj::Noise(stream) => stream.poll_read(cx, buf),
ConnectionProj::Raw(stream) => stream.poll_read(cx, buf),
ConnectionProj::Tcp(stream) => stream.poll_read(cx, buf),
}
}
}
impl<C> AsyncWrite for Connection<C>
where
C: AsyncWrite + AsyncRead + Unpin,
{
impl AsyncWrite for Connection {
fn poll_write(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
@@ -42,7 +48,7 @@ where
) -> std::task::Poll<Result<usize, io::Error>> {
match self.project() {
ConnectionProj::Noise(stream) => stream.poll_write(cx, buf),
ConnectionProj::Raw(stream) => stream.poll_write(cx, buf),
ConnectionProj::Tcp(stream) => stream.poll_write(cx, buf),
}
}
fn poll_flush(
@@ -51,7 +57,7 @@ where
) -> std::task::Poll<Result<(), io::Error>> {
match self.project() {
ConnectionProj::Noise(stream) => stream.poll_flush(cx),
ConnectionProj::Raw(stream) => stream.poll_flush(cx),
ConnectionProj::Tcp(stream) => stream.poll_flush(cx),
}
}
@@ -61,7 +67,7 @@ where
) -> std::task::Poll<Result<(), io::Error>> {
match self.project() {
ConnectionProj::Noise(stream) => stream.poll_shutdown(cx),
ConnectionProj::Raw(stream) => stream.poll_shutdown(cx),
ConnectionProj::Tcp(stream) => stream.poll_shutdown(cx),
}
}
}
+2 -54
View File
@@ -1,7 +1,6 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_noise_keys::NoiseVersion;
use snow::Error;
use std::io;
use thiserror::Error;
@@ -23,64 +22,13 @@ pub enum NoiseError {
#[error("Handshake did not complete")]
HandshakeError,
#[error("unknown noise version (encoded value: {encoded})")]
UnknownVersion { encoded: u8 },
#[error("unknown noise pattern (encoded value: {encoded})")]
UnknownPattern { encoded: u8 },
#[error("unknown noise message type (encoded value: {encoded})")]
UnknownMessageType { encoded: u8 },
#[error("failed to generate psk for requested version {noise_version}")]
PskGenerationFailure { noise_version: u8 },
#[error("noise initiator attempted to use version v{noise_version} of the protocol - we don't know how to handle it")]
UnknownVersionHandshake { noise_version: u8 },
#[error("noise initiator attempted to use an unexpected noise pattern. we're configured for {configured} while it requested {received}")]
UnexpectedNoisePattern {
configured: &'static str,
received: &'static str,
},
#[error("handshake version has unexpectedly changed. initial was {initial:?} and received {received:?}")]
UnexpectedHandshakeVersion {
initial: NoiseVersion,
received: NoiseVersion,
},
#[error("data packet version has unexpectedly changed. initial was {initial:?} and received {received:?}")]
UnexpectedDataVersion {
initial: NoiseVersion,
received: NoiseVersion,
},
#[error("received a non-handshake message during noise handshake")]
NonHandshakeMessageReceived,
#[error("received a non-data message post noise handshake")]
NonDataMessageReceived,
#[error("handshake message exceeded maximum size (got {size} bytes)")]
HandshakeTooBig { size: usize },
#[error("noise message exceeded maximum size (got {size} bytes)")]
DataTooBig { size: usize },
#[error("Unknown noise version")]
UnknownVersion,
#[error("Handshake timeout")]
HandshakeTimeout(#[from] tokio::time::error::Elapsed),
}
impl NoiseError {
pub(crate) fn naive_to_io_error(self) -> std::io::Error {
match self {
NoiseError::IoError(err) => err,
other => std::io::Error::other(other),
}
}
}
impl From<Error> for NoiseError {
fn from(err: Error) -> Self {
match err {
+79 -67
View File
@@ -1,63 +1,55 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::config::NoiseConfig;
use crate::connection::Connection;
use crate::error::NoiseError;
use crate::stream::NoiseStream;
use nym_crypto::asymmetric::x25519;
use nym_noise_keys::NoiseVersion;
use snow::error::Prerequisite;
use snow::Error;
use sha2::{Digest, Sha256};
use snow::{error::Prerequisite, Error};
use tokio::net::TcpStream;
use tracing::{error, warn};
use tracing::*;
pub mod config;
pub mod connection;
pub mod error;
pub mod stream;
use crate::config::NoiseConfig;
use crate::connection::Connection;
use crate::error::NoiseError;
use crate::stream::NoiseStreamBuilder;
const NOISE_PSK_PREFIX: &[u8] = b"NYMTECH_NOISE_dQw4w9WgXcQ";
pub const LATEST_NOISE_VERSION: NoiseVersion = NoiseVersion::V1;
// TODO: this should be behind some trait because presumably, depending on the version,
// other arguments would be needed
mod psk_gen {
use crate::error::NoiseError;
use crate::stream::Psk;
use crate::NOISE_PSK_PREFIX;
use nym_crypto::asymmetric::x25519;
use nym_noise_keys::NoiseVersion;
use sha2::{Digest, Sha256};
fn generate_psk_v1(responder_pub_key: &x25519::PublicKey) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(NOISE_PSK_PREFIX);
hasher.update(responder_pub_key.to_bytes());
hasher.finalize().into()
}
pub(crate) fn generate_psk(
responder_pub_key: x25519::PublicKey,
version: NoiseVersion,
) -> Result<Psk, NoiseError> {
match version {
NoiseVersion::V1 => Ok(generate_psk_v1(responder_pub_key)),
NoiseVersion::Unknown(noise_version) => {
Err(NoiseError::PskGenerationFailure { noise_version })
}
}
}
async fn upgrade_noise_initiator_v1(
conn: TcpStream,
config: &NoiseConfig,
remote_pub_key: &x25519::PublicKey,
) -> Result<Connection, NoiseError> {
trace!("Perform Noise Handshake, initiator side");
fn generate_psk_v1(responder_pub_key: x25519::PublicKey) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(NOISE_PSK_PREFIX);
hasher.update(responder_pub_key.to_bytes());
hasher.finalize().into()
}
let secret_hash = generate_psk_v1(remote_pub_key);
let noise_stream = NoiseStream::new_initiator(conn, config, remote_pub_key, &secret_hash)?;
Ok(Connection::Noise(
tokio::time::timeout(config.timeout, noise_stream.perform_handshake()).await??,
))
}
pub async fn upgrade_noise_initiator(
conn: TcpStream,
config: &NoiseConfig,
) -> Result<Connection<TcpStream>, NoiseError> {
) -> Result<Connection, NoiseError> {
if config.unsafe_disabled {
warn!("Noise is disabled in the config. Not attempting any handshake");
return Ok(Connection::Raw(conn));
return Ok(Connection::Tcp(conn));
}
//Get init material
@@ -66,34 +58,46 @@ pub async fn upgrade_noise_initiator(
Error::Prereq(Prerequisite::RemotePublicKey)
})?;
let Some(key) = config.get_noise_key(&responder_addr) else {
warn!("{responder_addr} can't speak Noise yet, falling back to TCP");
return Ok(Connection::Raw(conn));
};
let handshake_version = match key.supported_version {
NoiseVersion::V1 => NoiseVersion::V1,
// We're talking to a more recent node, but we can't adapt. Let's try to do our best and if it fails, it fails.
// If that node sees we're older, it will try to adapt too.
NoiseVersion::Unknown(version) => {
warn!("{responder_addr} is announcing an v{version} version of Noise that we don't know how to parse, we will attempt to downgrade to our current highest supported version");
LATEST_NOISE_VERSION
match config.get_noise_key(&responder_addr) {
Some(key) => match key.version {
NoiseVersion::V1 => upgrade_noise_initiator_v1(conn, config, &key.x25519_pubkey).await,
// We're talking to a more recent node, but we can't adapt. Let's try to do our best and if it fails, it fails.
// If that node sees we're older, it will try to adapt too.
NoiseVersion::Unknown => {
warn!("{responder_addr} is announcing an unknown version of Noise, we will still attempt our latest known version");
upgrade_noise_initiator_v1(conn, config, &key.x25519_pubkey)
.await
.or(Err(NoiseError::UnknownVersion))
}
},
None => {
warn!("{responder_addr} can't speak Noise yet, falling back to TCP");
Ok(Connection::Tcp(conn))
}
};
NoiseStreamBuilder::new(conn)
.perform_initiator_handshake(config, handshake_version, key.x25519_pubkey)
.await
.map(|stream| Connection::Noise(Box::new(stream)))
}
}
async fn upgrade_noise_responder_v1(
conn: TcpStream,
config: &NoiseConfig,
) -> Result<Connection, NoiseError> {
trace!("Perform Noise Handshake, responder side");
let secret_hash = generate_psk_v1(config.local_key.public_key());
let noise_stream = NoiseStream::new_responder(conn, config, &secret_hash)?;
Ok(Connection::Noise(
tokio::time::timeout(config.timeout, noise_stream.perform_handshake()).await??,
))
}
pub async fn upgrade_noise_responder(
conn: TcpStream,
config: &NoiseConfig,
) -> Result<Connection<TcpStream>, NoiseError> {
) -> Result<Connection, NoiseError> {
if config.unsafe_disabled {
warn!("Noise is disabled in the config. Not attempting any handshake");
return Ok(Connection::Raw(conn));
return Ok(Connection::Tcp(conn));
}
//Get init material
@@ -105,14 +109,22 @@ pub async fn upgrade_noise_responder(
}
};
// if responder doesn't announce noise support, we fallback to tcp
if config.get_noise_support(initiator_addr.ip()).is_none() {
warn!("{initiator_addr} can't speak Noise yet, falling back to TCP",);
return Ok(Connection::Raw(conn));
};
NoiseStreamBuilder::new(conn)
.perform_responder_handshake(config)
.await
.map(|stream| Connection::Noise(Box::new(stream)))
// Port is random and we just need the support info
match config.get_noise_support(initiator_addr.ip()) {
None => {
warn!("{initiator_addr} can't speak Noise yet, falling back to TCP",);
Ok(Connection::Tcp(conn))
}
// responder's info on version is shaky, so ideally, initiator has to adapt.
// if we are newer, it won't ba able to, so let's try to meet him on his ground.
Some(LATEST_NOISE_VERSION) | Some(NoiseVersion::Unknown) => {
// Node is announcing the same version as us, great or
// Node is announcing a newer version than us, it should adapt to us though
upgrade_noise_responder_v1(conn, config).await
} //SW sample of code to allow backwards compatibility when we introduce new versions
// Some(IntermediateNoiseVersion) => {
// Node is announcing an older version, let's try to adapt
// upgrade_noise_responder_Vwhatever
// }
}
}
+224
View File
@@ -0,0 +1,224 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{config::NoiseConfig, error::NoiseError};
use bytes::BytesMut;
use futures::{Sink, SinkExt, Stream, StreamExt};
use nym_crypto::asymmetric::x25519;
use pin_project::pin_project;
use snow::{Builder, HandshakeState, TransportState};
use std::io;
use std::pin::Pin;
use std::task::Poll;
use std::{cmp::min, task::ready};
use tokio::{
io::{AsyncRead, AsyncWrite, ReadBuf},
net::TcpStream,
};
use tokio_util::codec::{Framed, LengthDelimitedCodec};
const TAGLEN: usize = 16;
const HANDSHAKE_MAX_LEN: usize = 1024; // using this constant to limit the handshake's buffer size
pub(crate) type Psk = [u8; 32];
/// Wrapper around a TcpStream
#[pin_project]
pub struct NoiseStream {
#[pin]
inner_stream: Framed<TcpStream, LengthDelimitedCodec>,
handshake: Option<HandshakeState>,
noise: Option<TransportState>,
dec_buffer: BytesMut,
}
impl NoiseStream {
pub(crate) fn new_initiator(
inner_stream: TcpStream,
config: &NoiseConfig,
remote_pub_key: &x25519::PublicKey,
psk: &Psk,
) -> Result<NoiseStream, NoiseError> {
let handshake = Builder::new(config.pattern.as_noise_params())
.local_private_key(config.local_key.private_key().as_bytes())
.remote_public_key(&remote_pub_key.to_bytes())
.psk(config.pattern.psk_position(), psk)
.build_initiator()?;
Ok(NoiseStream::new_inner(inner_stream, handshake))
}
pub(crate) fn new_responder(
inner_stream: TcpStream,
config: &NoiseConfig,
psk: &Psk,
) -> Result<NoiseStream, NoiseError> {
let handshake = Builder::new(config.pattern.as_noise_params())
.local_private_key(config.local_key.private_key().as_bytes())
.psk(config.pattern.psk_position(), psk)
.build_responder()?;
Ok(NoiseStream::new_inner(inner_stream, handshake))
}
fn new_inner(inner_stream: TcpStream, handshake: HandshakeState) -> NoiseStream {
NoiseStream {
inner_stream: LengthDelimitedCodec::builder()
.length_field_type::<u16>()
.new_framed(inner_stream),
handshake: Some(handshake),
noise: None,
dec_buffer: BytesMut::new(),
}
}
pub(crate) async fn perform_handshake(mut self) -> Result<Self, NoiseError> {
//Check if we are in the correct state
let Some(mut handshake) = self.handshake.take() else {
return Err(NoiseError::IncorrectStateError);
};
while !handshake.is_handshake_finished() {
if handshake.is_my_turn() {
self.send_handshake_msg(&mut handshake).await?;
} else {
self.recv_handshake_msg(&mut handshake).await?;
}
}
self.noise = Some(handshake.into_transport_mode()?);
Ok(self)
}
async fn send_handshake_msg(
&mut self,
handshake: &mut HandshakeState,
) -> Result<(), NoiseError> {
let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN); // we're in the handshake, we can afford a smaller buffer
let len = handshake.write_message(&[], &mut buf)?;
buf.truncate(len);
self.inner_stream.send(buf.into()).await?;
Ok(())
}
async fn recv_handshake_msg(
&mut self,
handshake: &mut HandshakeState,
) -> Result<(), NoiseError> {
match self.inner_stream.next().await {
Some(Ok(msg)) => {
let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN); // we're in the handshake, we can afford a smaller buffer
handshake.read_message(&msg, &mut buf)?;
Ok(())
}
Some(Err(err)) => Err(NoiseError::IoError(err)),
None => Err(NoiseError::HandshakeError),
}
}
pub fn peer_addr(&self) -> Result<std::net::SocketAddr, io::Error> {
self.inner_stream.get_ref().peer_addr()
}
}
impl AsyncRead for NoiseStream {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
let projected_self = self.project();
let pending = match projected_self.inner_stream.poll_next(cx) {
Poll::Pending => {
//no new data, a return value of Poll::Pending means the waking is already scheduled
//Nothing new to decrypt, only check if we can return something from dec_storage, happens after
true
}
Poll::Ready(Some(Ok(noise_msg))) => {
// We have a new noise msg
let mut dec_msg = BytesMut::zeroed(noise_msg.len() - TAGLEN);
let len = match projected_self.noise {
Some(transport_state) => {
match transport_state.read_message(&noise_msg, &mut dec_msg) {
Ok(len) => len,
Err(_) => return Poll::Ready(Err(io::ErrorKind::InvalidInput.into())),
}
}
None => return Poll::Ready(Err(io::ErrorKind::Other.into())),
};
projected_self.dec_buffer.extend(&dec_msg[..len]);
false
}
Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(err)),
Poll::Ready(None) => {
//Stream is done, we might still have data in the buffer though, happens afterwards
false
}
};
// Checking if there is something to return from the buffer
let read_len = min(buf.remaining(), projected_self.dec_buffer.len());
if read_len > 0 {
buf.put_slice(&projected_self.dec_buffer.split_to(read_len));
return Poll::Ready(Ok(()));
}
// buf.remaining == 0 or nothing in the buffer, we must return the value we had from the inner_stream
if pending {
//If we end up here, it means the previous poll_next was pending as well, hence waking is already scheduled
Poll::Pending
} else {
Poll::Ready(Ok(()))
}
}
}
impl AsyncWrite for NoiseStream {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
let mut projected_self = self.project();
// returns on Poll::Pending and Poll:Ready(Err)
ready!(projected_self.inner_stream.as_mut().poll_ready(cx))?;
// Ready to send, encrypting message
let mut noise_buf = BytesMut::zeroed(buf.len() + TAGLEN);
let Ok(len) = (match projected_self.noise {
Some(transport_state) => transport_state.write_message(buf, &mut noise_buf),
None => return Poll::Ready(Err(io::ErrorKind::Other.into())),
}) else {
return Poll::Ready(Err(io::ErrorKind::InvalidInput.into()));
};
noise_buf.truncate(len);
// Tokio uses the same `start_send ` in their SinkWriter implementation. https://docs.rs/tokio-util/latest/src/tokio_util/io/sink_writer.rs.html#104
match projected_self
.inner_stream
.as_mut()
.start_send(noise_buf.into())
{
Ok(()) => Poll::Ready(Ok(buf.len())),
Err(e) => Poll::Ready(Err(e)),
}
}
fn poll_flush(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
self.project().inner_stream.poll_flush(cx)
}
fn poll_shutdown(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
self.project().inner_stream.poll_close(cx)
}
}
+1 -26
View File
@@ -895,39 +895,15 @@ pub struct HostKeys {
pub current_x25519_sphinx_key: SphinxKey,
#[serde(default)]
pub pre_announced_x25519_sphinx_key: Option<SphinxKey>,
#[serde(default)]
pub x25519_versioned_noise: Option<VersionedNoiseKey>,
}
#[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 {
HostKeys {
ed25519: value.ed25519_identity,
x25519: value.x25519_sphinx,
current_x25519_sphinx_key: value.primary_x25519_sphinx_key.into(),
pre_announced_x25519_sphinx_key: value.pre_announced_x25519_sphinx_key.map(Into::into),
x25519_versioned_noise: value.x25519_versioned_noise,
}
}
@@ -1108,11 +1084,10 @@ impl NymNodeDescription {
pub fn to_semi_skimmed_node(
&self,
current_rotation_id: u32,
role: NodeRole,
performance: Performance,
) -> SemiSkimmedNode {
let skimmed_node = self.to_skimmed_node(current_rotation_id, role, performance);
let skimmed_node = self.to_skimmed_node(role, performance);
SemiSkimmedNode {
basic: skimmed_node,
+1 -1
View File
@@ -8,7 +8,7 @@ use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_mixnet_contract_common::nym_node::Role;
use nym_mixnet_contract_common::reward_params::Performance;
use nym_mixnet_contract_common::{EpochId, Interval, NodeId};
use nym_mixnet_contract_common::{Interval, NodeId};
use nym_noise_keys::VersionedNoiseKey;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -1,13 +1,97 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
use crate::node_describe_cache::DescribedNodes;
use crate::node_status_api::models::AxumResult;
use crate::nym_nodes::handlers::unstable::helpers::{refreshed_at, LegacyAnnotation};
use crate::nym_nodes::handlers::unstable::NodesParamsWithRole;
use crate::support::http::state::AppState;
use crate::unstable_routes::v1::nym_nodes::helpers::NodesParamsWithRole;
use axum::extract::{Query, State};
use nym_api_requests::nym_nodes::{CachedNodesResponse, SemiSkimmedNode};
use nym_api_requests::models::{
NodeAnnotation, NymNodeDescription, OffsetDateTimeJsonSchemaWrapper,
};
use nym_api_requests::nym_nodes::{NodeRole, PaginatedCachedNodesResponse, SemiSkimmedNode};
use nym_api_requests::pagination::PaginatedResponse;
use nym_http_api_common::FormattedResponse;
use nym_mixnet_contract_common::NodeId;
use nym_topology::CachedEpochRewardedSet;
use std::collections::HashMap;
use tracing::trace;
use utoipa::ToSchema;
pub type PaginatedSemiSkimmedNodes =
AxumResult<FormattedResponse<PaginatedCachedNodesResponse<SemiSkimmedNode>>>;
//SW TODO : this is copied from skimmed nodes, surely we can do better than that
fn build_nym_nodes_response<'a, NI>(
rewarded_set: &CachedEpochRewardedSet,
nym_nodes_subset: NI,
annotations: &HashMap<NodeId, NodeAnnotation>,
active_only: bool,
) -> Vec<SemiSkimmedNode>
where
NI: Iterator<Item = &'a NymNodeDescription> + 'a,
{
let mut nodes = Vec::new();
for nym_node in nym_nodes_subset {
let node_id = nym_node.node_id;
let role: NodeRole = rewarded_set.role(node_id).into();
// if the role is inactive, see if our filter allows it
if active_only && role.is_inactive() {
continue;
}
// honestly, not sure under what exact circumstances this value could be missing,
// but in that case just use 0 performance
let annotation = annotations.get(&node_id).copied().unwrap_or_default();
nodes.push(nym_node.to_semi_skimmed_node(role, annotation.last_24h_performance));
}
nodes
}
//SW TODO : this is copied from skimmed nodes, surely we can do better than that
/// Given all relevant caches, add appropriate legacy nodes to the part of the response
fn add_legacy<LN>(
nodes: &mut Vec<SemiSkimmedNode>,
rewarded_set: &CachedEpochRewardedSet,
describe_cache: &DescribedNodes,
annotated_legacy_nodes: &HashMap<NodeId, LN>,
) where
LN: LegacyAnnotation,
{
for (node_id, legacy) in annotated_legacy_nodes.iter() {
let role: NodeRole = rewarded_set.role(*node_id).into();
// if we have self-described info, prefer it over contract data
if let Some(described) = describe_cache.get_node(node_id) {
nodes.push(described.to_semi_skimmed_node(role, legacy.performance()))
} else {
match legacy.try_to_semi_skimmed_node(role) {
Ok(node) => nodes.push(node),
Err(err) => {
let id = legacy.identity();
trace!("node {id} is malformed: {err}")
}
}
}
}
}
#[allow(dead_code)] // not dead, used in OpenAPI docs
#[derive(ToSchema)]
#[schema(title = "PaginatedCachedNodesExpandedResponseSchema")]
pub struct PaginatedCachedNodesExpandedResponseSchema {
pub refreshed_at: OffsetDateTimeJsonSchemaWrapper,
#[schema(value_type = SemiSkimmedNode)]
pub nodes: PaginatedResponse<SemiSkimmedNode>,
}
/// Return all Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used)
/// that are currently bonded.
#[utoipa::path(
tag = "Unstable Nym Nodes",
get,
@@ -15,13 +99,43 @@ use nym_http_api_common::FormattedResponse;
path = "",
context_path = "/v1/unstable/nym-nodes/semi-skimmed",
responses(
// (status = 200, body = CachedNodesResponse<SemiSkimmedNode>)
(status = 501)
(status = 200, content(
(PaginatedCachedNodesExpandedResponseSchema = "application/json"),
(PaginatedCachedNodesExpandedResponseSchema = "application/yaml"),
(PaginatedCachedNodesExpandedResponseSchema = "application/bincode")
))
)
)]
pub(crate) async fn nodes_expanded(
_state: State<AppState>,
_query_params: Query<NodesParamsWithRole>,
) -> AxumResult<FormattedResponse<CachedNodesResponse<SemiSkimmedNode>>> {
Err(AxumErrorResponse::not_implemented())
pub(super) async fn nodes_expanded(
state: State<AppState>,
query_params: Query<NodesParamsWithRole>,
) -> PaginatedSemiSkimmedNodes {
// 1. grab all relevant described nym-nodes
let rewarded_set = state.rewarded_set().await?;
let describe_cache = state.describe_nodes_cache_data().await?;
let all_nym_nodes = describe_cache.all_nym_nodes();
let annotations = state.node_annotations().await?;
let legacy_mixnodes = state.legacy_mixnode_annotations().await?;
let legacy_gateways = state.legacy_gateways_annotations().await?;
let mut nodes = build_nym_nodes_response(&rewarded_set, all_nym_nodes, &annotations, false);
// add legacy gateways to the response
add_legacy(&mut nodes, &rewarded_set, &describe_cache, &legacy_gateways);
// add legacy mixnodes to the response
add_legacy(&mut nodes, &rewarded_set, &describe_cache, &legacy_mixnodes);
// min of all caches
let refreshed_at = refreshed_at([
rewarded_set.timestamp(),
annotations.timestamp(),
describe_cache.timestamp(),
legacy_mixnodes.timestamp(),
legacy_gateways.timestamp(),
]);
let output = query_params.output.unwrap_or_default();
Ok(output.to_response(PaginatedCachedNodesResponse::new_full(refreshed_at, nodes)))
}
+128 -32
View File
@@ -10,6 +10,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::ops::Deref;
use v1::node::models::LegacyHostInformationV3;
#[cfg(feature = "client")]
pub mod client;
@@ -130,7 +131,7 @@ impl Display for ErrorResponse {
mod tests {
use super::*;
use crate::api::v1::node::models::{HostKeys, SphinxKey};
use crate::api::v1::node::models::HostInformation;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_noise_keys::{NoiseVersion, VersionedNoiseKey};
use rand_chacha::rand_core::SeedableRng;
@@ -140,9 +141,8 @@ 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_versioned_noise = VersionedNoiseKey {
supported_version: NoiseVersion::V1,
version: NoiseVersion::V1,
x25519_pubkey: *x25519::KeyPair::new(&mut rng).public_key(),
};
@@ -155,11 +155,6 @@ mod tests {
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: *ed22519.public_key(),
x25519_sphinx: *x25519_sphinx.public_key(),
primary_x25519_sphinx_key: SphinxKey {
rotation_id: current_rotation_id,
public_key: *x25519_sphinx.public_key(),
},
pre_announced_x25519_sphinx_key: None,
x25519_versioned_noise: None,
},
};
@@ -174,11 +169,6 @@ mod tests {
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: *ed22519.public_key(),
x25519_sphinx: *x25519_sphinx.public_key(),
primary_x25519_sphinx_key: SphinxKey {
rotation_id: current_rotation_id,
public_key: *x25519_sphinx.public_key(),
},
pre_announced_x25519_sphinx_key: None,
x25519_versioned_noise: Some(x25519_versioned_noise),
},
};
@@ -329,6 +319,84 @@ mod tests {
assert!(current_struct_noise.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_no_noise = 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: None,
},
};
//technically this variant should never happen
let legacy_info_noise = 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()),
},
};
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,
x25519_sphinx: legacy_info_no_noise.keys.x25519_sphinx,
x25519_versioned_noise: None,
},
};
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,
x25519_sphinx: legacy_info_noise.keys.x25519_sphinx,
x25519_versioned_noise: Some(VersionedNoiseKey {
version: NoiseVersion::V1,
x25519_pubkey: legacy_info_noise.keys.x25519_noise.unwrap(),
}),
},
};
// signature on legacy data
let signature_no_noise = SignedData::new(legacy_info_no_noise, ed22519.private_key())
.unwrap()
.signature;
let signature_noise = SignedData::new(legacy_info_noise, ed22519.private_key())
.unwrap()
.signature;
// signed blob with the 'current' structure
let current_struct_no_noise = SignedData {
data: host_info_no_noise,
signature: signature_no_noise,
};
let current_struct_noise = SignedData {
data: host_info_noise,
signature: signature_noise,
};
assert!(!current_struct_no_noise.verify(ed22519.public_key()));
assert!(current_struct_no_noise.verify_host_information());
assert!(!current_struct_noise.verify(ed22519.public_key()));
assert!(current_struct_noise.verify_host_information())
}
#[test]
fn dummy_legacy_v2_signed_host_verification() {
let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]);
@@ -362,12 +430,7 @@ mod tests {
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: *x25519_sphinx.public_key(),
primary_x25519_sphinx_key: SphinxKey {
rotation_id: u32::MAX,
public_key: *x25519_sphinx.public_key(),
},
pre_announced_x25519_sphinx_key: None,
x25519_sphinx: legacy_info_no_noise.keys.x25519_sphinx.parse().unwrap(),
x25519_versioned_noise: None,
},
};
@@ -378,14 +441,9 @@ mod tests {
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: *x25519_sphinx.public_key(),
primary_x25519_sphinx_key: SphinxKey {
rotation_id: u32::MAX,
public_key: *x25519_sphinx.public_key(),
},
pre_announced_x25519_sphinx_key: None,
x25519_sphinx: legacy_info_noise.keys.x25519_sphinx.parse().unwrap(),
x25519_versioned_noise: Some(VersionedNoiseKey {
supported_version: NoiseVersion::V1,
version: NoiseVersion::V1,
x25519_pubkey: legacy_info_noise.keys.x25519_noise.parse().unwrap(),
}),
},
@@ -418,6 +476,49 @@ mod tests {
assert!(current_struct_noise.verify_host_information())
}
#[test]
fn dummy_current_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 host_info_no_noise = 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(),
x25519_versioned_noise: None,
},
};
let host_info_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(),
x25519_sphinx: *x25519_sphinx.public_key(),
x25519_versioned_noise: Some(VersionedNoiseKey {
version: NoiseVersion::V1,
x25519_pubkey: *x25519_noise.public_key(),
}),
},
};
// signature on legacy data
let current_struct_no_noise =
SignedData::new(host_info_no_noise, ed22519.private_key()).unwrap();
let current_struct_noise = SignedData::new(host_info_noise, ed22519.private_key()).unwrap();
assert!(current_struct_no_noise.verify(ed22519.public_key()));
assert!(current_struct_no_noise.verify_host_information());
// if noise key is present, the signature is actually valid
assert!(current_struct_noise.verify(ed22519.public_key()));
assert!(current_struct_noise.verify_host_information())
}
#[test]
fn dummy_legacy_v1_signed_host_verification() {
let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]);
@@ -439,12 +540,7 @@ mod tests {
hostname: legacy_info.hostname.clone(),
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: legacy_info.keys.ed25519.parse().unwrap(),
x25519_sphinx: *x25519_sphinx.public_key(),
primary_x25519_sphinx_key: SphinxKey {
rotation_id: u32::MAX,
public_key: *x25519_sphinx.public_key(),
},
pre_announced_x25519_sphinx_key: None,
x25519_sphinx: legacy_info.keys.x25519.parse().unwrap(),
x25519_versioned_noise: None,
},
};
@@ -149,77 +149,6 @@ pub struct HostKeys {
pub x25519_versioned_noise: Option<VersionedNoiseKey>,
}
// we need the intermediate struct to help us with the new explicit sphinx key fields
#[allow(deprecated)]
impl From<HostKeysDeHelper> for HostKeys {
fn from(value: HostKeysDeHelper) -> Self {
let primary_x25519_sphinx_key = match value.primary_x25519_sphinx_key {
None => {
// legacy
SphinxKey::new_legacy(value.x25519_sphinx)
}
Some(primary_x25519_sphinx_key) => primary_x25519_sphinx_key,
};
HostKeys {
ed25519_identity: value.ed25519_identity,
x25519_sphinx: value.x25519_sphinx,
primary_x25519_sphinx_key,
pre_announced_x25519_sphinx_key: value.pre_announced_x25519_sphinx_key,
x25519_versioned_noise: value.x25519_versioned_noise,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
struct HostKeysDeHelper {
/// 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,
#[deprecated(note = "use explicit primary_x25519_sphinx_key instead")]
#[serde(alias = "x25519")]
#[serde(with = "bs58_x25519_pubkey")]
pub x25519_sphinx: x25519::PublicKey,
/// Current, active, x25519 sphinx key clients are expected to use when constructing packets
/// with this node in the route.
pub primary_x25519_sphinx_key: Option<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)]
pub x25519_versioned_noise: Option<VersionedNoiseKey>,
}
#[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,
}
impl SphinxKey {
pub fn new_legacy(public_key: x25519::PublicKey) -> SphinxKey {
SphinxKey {
rotation_id: u32::MAX,
public_key,
}
}
pub fn is_legacy(&self) -> bool {
self.rotation_id == u32::MAX
}
}
#[derive(Serialize)]
pub struct LegacyHostKeysV3 {
#[serde(alias = "ed25519")]
@@ -252,7 +181,7 @@ impl From<HostKeys> for LegacyHostKeysV3 {
fn from(value: HostKeys) -> Self {
LegacyHostKeysV3 {
ed25519_identity: value.ed25519_identity,
x25519_sphinx: value.primary_x25519_sphinx_key.public_key,
x25519_sphinx: value.x25519_sphinx,
x25519_noise: value.x25519_versioned_noise.map(|k| k.x25519_pubkey),
}
}
+35
View File
@@ -1,4 +1,39 @@
// 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;
use nym_noise_keys::VersionedNoiseKey;
pub mod system_info;
pub(crate) fn sign_host_details(
config: &Config,
x22519_sphinx: &x25519::PublicKey,
x25519_versioned_noise: &VersionedNoiseKey,
ed22519_identity: &ed25519::KeyPair,
) -> Result<SignedHostInformation, NymNodeError> {
let x25519_versioned_noise = if config.mixnet.debug.unsafe_disable_noise {
None
} else {
Some(*x25519_versioned_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_versioned_noise,
},
};
let signed_info = SignedHostInformation::new(host_info, ed22519_identity.private_key())
.map_err(NymNodeHttpError::from)?;
Ok(signed_info)
}
+1 -1
View File
@@ -484,7 +484,7 @@ impl ConnectionHandler {
pub(crate) async fn handle_stream(
&mut self,
mut mixnet_connection: Framed<Connection<TcpStream>, NymCodec>,
mut mixnet_connection: Framed<Connection, NymCodec>,
) {
loop {
tokio::select! {
+10 -2
View File
@@ -702,6 +702,16 @@ impl NymNode {
}
pub(crate) async fn build_http_server(&self) -> Result<NymNodeHttpServer, NymNodeError> {
let host_details = sign_host_details(
&self.config,
self.x25519_sphinx_keys.public_key(),
&VersionedNoiseKey {
version: nym_noise::LATEST_NOISE_VERSION,
x25519_pubkey: *self.x25519_noise_keys.public_key(),
},
&self.ed25519_identity_keys,
)?;
let auxiliary_details = api_requests::v1::node::models::AuxiliaryDetails {
location: self.config.host.location,
announce_ports: AnnouncePorts {
@@ -1151,8 +1161,6 @@ impl NymNode {
let network_refresher = self.build_network_refresher().await?;
let active_clients_store = ActiveClientsStore::new();
let bloomfilters_manager = self.setup_replay_detection().await?;
let noise_config = nym_noise::config::NoiseConfig::new(
self.x25519_noise_keys.clone(),
network_refresher.noise_view(),
+16 -26
View File
@@ -16,13 +16,10 @@ use nym_topology::{
TopologyProvider,
};
use nym_validator_client::nym_api::NymApiClientExt;
use nym_validator_client::nym_nodes::{
NodesByAddressesResponse, SemiSkimmedNode, SemiSkimmedNodesWithMetadata,
};
use nym_validator_client::nym_nodes::{NodesByAddressesResponse, SemiSkimmedNode};
use nym_validator_client::{NymApiClient, ValidatorClientError};
use std::collections::{HashMap, HashSet};
use std::net::{IpAddr, SocketAddr};
use std::ops::Deref;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
@@ -64,9 +61,7 @@ impl NodesQuerier {
res
}
async fn current_nymnodes(
&mut self,
) -> Result<SemiSkimmedNodesWithMetadata, ValidatorClientError> {
async fn current_nymnodes(&mut self) -> Result<Vec<SemiSkimmedNode>, ValidatorClientError> {
let res = self
.client
.get_all_expanded_nodes()
@@ -148,24 +143,20 @@ impl TopologyProvider for CachedTopologyProvider {
let network_guard = self.cached_network.inner.read().await;
let self_node = self.gateway_node.identity_key;
let mut topology = NymTopology::new(
network_guard.topology_metadata,
network_guard.rewarded_set.clone(),
Vec::new(),
)
.with_additional_nodes(
network_guard
.network_nodes
.iter()
.map(|node| &node.basic)
.filter(|node| {
if node.supported_roles.mixnode {
node.performance.round_to_integer() >= self.min_mix_performance
} else {
true
}
}),
);
let mut topology = NymTopology::new_empty(network_guard.rewarded_set.clone())
.with_additional_nodes(
network_guard
.network_nodes
.iter()
.map(|node| &node.basic)
.filter(|node| {
if node.supported_roles.mixnode {
node.performance.round_to_integer() >= self.min_mix_performance
} else {
true
}
}),
);
if !topology.has_node(self.gateway_node.identity_key) {
debug!("{self_node} didn't exist in topology. inserting it.",);
@@ -196,7 +187,6 @@ impl CachedNetwork {
struct CachedNetworkInner {
rewarded_set: EpochRewardedSet,
topology_metadata: NymTopologyMetadata,
network_nodes: Vec<SemiSkimmedNode>,
}