Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a7587eafb7 | |||
| 6710e8df56 | |||
| a5ba5e57ad | |||
| 389d7d0b4e | |||
| 650c034f46 | |||
| 9ca0ba802b | |||
| 118f836690 | |||
| afbdc107bf | |||
| a66852c0ae | |||
| 04e4378352 | |||
| 2b056fdd6e | |||
| c20dcd7de9 | |||
| 53caaf3ac7 | |||
| 98c7b57468 | |||
| 77e1468aae | |||
| 00d509ca79 | |||
| db5692ffc7 | |||
| 8151f65963 | |||
| 8b24e426bf | |||
| 507f40a963 | |||
| e06db2f18e | |||
| 8b23552e77 | |||
| 74b2aab446 | |||
| fb874d699b | |||
| 764639971c |
Generated
+290
-32
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
¶ms,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[deprecated]
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_active_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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! {
|
||||
|
||||
@@ -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,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>,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user