Merge pull request #6513 from nymtech/bugfix/lp-psqv2-review-comments
addressing LP PR comments
This commit is contained in:
Generated
+1
-1
@@ -6875,7 +6875,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-lp"
|
||||
version = "0.1.0"
|
||||
version = "1.20.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bs58",
|
||||
|
||||
+3
-1
@@ -202,7 +202,7 @@ homepage = "https://nymtech.net"
|
||||
documentation = "https://nymtech.net"
|
||||
edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
rust-version = "1.85"
|
||||
rust-version = "1.87.0"
|
||||
readme = "README.md"
|
||||
version = "1.20.4"
|
||||
|
||||
@@ -448,8 +448,10 @@ nym-http-api-common = { version = "1.20.4", path = "common/http-api-common", def
|
||||
nym-id = { version = "1.20.4", path = "common/nym-id" }
|
||||
nym-ip-packet-client = { version = "1.20.4", path = "nym-ip-packet-client" }
|
||||
nym-ip-packet-requests = { version = "1.20.4", path = "common/ip-packet-requests" }
|
||||
nym-lp = { version = "1.20.4", path = "common/nym-lp" }
|
||||
nym-kkt = { version = "0.1.0", path = "common/nym-kkt" }
|
||||
nym-kkt-ciphersuite = { version = "1.20.4", path = "common/nym-kkt-ciphersuite" }
|
||||
nym-kkt-context = { version = "1.20.4", path = "common/nym-kkt-context" }
|
||||
nym-metrics = { version = "1.20.4", path = "common/nym-metrics" }
|
||||
nym-mixnet-client = { version = "1.20.4", path = "common/client-libs/mixnet-client" }
|
||||
nym-mixnet-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
|
||||
@@ -511,14 +511,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_key_conversion() {
|
||||
let dalek_kp = super::KeyPair::new(&mut rand::thread_rng());
|
||||
let dalek_kp = KeyPair::new(&mut rand::thread_rng());
|
||||
|
||||
let mut dalek_private_key_bytes = dalek_kp.private_key().as_bytes().to_owned();
|
||||
|
||||
libcrux_curve25519::clamp(&mut dalek_private_key_bytes);
|
||||
let libcrux_private_key =
|
||||
libcrux_psq::handshake::types::DHPrivateKey::from_bytes(&dalek_private_key_bytes)
|
||||
.unwrap();
|
||||
let libcrux_private_key = DHPrivateKey::from_bytes(&dalek_private_key_bytes).unwrap();
|
||||
let libcrux_public_key = libcrux_private_key.to_public();
|
||||
|
||||
assert_eq!(libcrux_public_key.as_ref(), dalek_kp.public_key.as_bytes());
|
||||
|
||||
@@ -12,9 +12,9 @@ num_enum = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
|
||||
# internal
|
||||
nym-crypto = { path = "../crypto", features = ["hashing"] }
|
||||
nym-crypto = { workspace = true, features = ["hashing"] }
|
||||
nym-kkt-ciphersuite = { workspace = true, features = ["digests"] }
|
||||
nym-kkt-context = { path = "../nym-kkt-context" }
|
||||
nym-kkt-context = { workspace = true }
|
||||
nym-pemstore = { workspace = true }
|
||||
|
||||
libcrux-kem = { workspace = true }
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::error::KKTError;
|
||||
pub const MAX_PAYLOAD_LEN: usize = 1_000_000;
|
||||
const CARRIER_KDF_INFO_TX: &str = "CARRIER_V1_KDF_TX";
|
||||
const CARRIER_KDF_INFO_RX: &str = "CARRIER_V1_KDF_RX";
|
||||
const CARRIER_KKT_AAD: &[u8] = b"kkt-carrier-v1";
|
||||
|
||||
#[derive(Zeroize, ZeroizeOnDrop)]
|
||||
pub struct Carrier {
|
||||
@@ -107,7 +108,7 @@ impl Carrier {
|
||||
&self.tx_key,
|
||||
plaintext,
|
||||
&mut output_buffer,
|
||||
b"kkt-carrier-v1",
|
||||
CARRIER_KKT_AAD,
|
||||
&as_nonce_bytes(self.tx_counter),
|
||||
)?;
|
||||
|
||||
@@ -126,7 +127,7 @@ impl Carrier {
|
||||
&self.rx_key,
|
||||
&mut output_buffer,
|
||||
ciphertext,
|
||||
b"kkt-carrier-v1",
|
||||
CARRIER_KKT_AAD,
|
||||
&as_nonce_bytes(self.rx_counter),
|
||||
)?;
|
||||
|
||||
|
||||
+11
-11
@@ -16,40 +16,40 @@ pub enum KKTError {
|
||||
#[error(transparent)]
|
||||
MaskedByteError(#[from] MaskedByteError),
|
||||
|
||||
#[error("KEM mapping failure: {}", info)]
|
||||
#[error("KEM mapping failure: {info}")]
|
||||
KEMMapping { info: &'static str },
|
||||
|
||||
#[error("Insecure Encapsulation Key Hash Length")]
|
||||
InsecureHashLen,
|
||||
|
||||
#[error("KKT Frame Decoding Error: {}", info)]
|
||||
#[error("KKT Frame Decoding Error: {info}")]
|
||||
FrameDecodingError { info: String },
|
||||
|
||||
#[error("KKT Frame Encoding Error: {}", info)]
|
||||
#[error("KKT Frame Encoding Error: {info}")]
|
||||
FrameEncodingError { info: String },
|
||||
|
||||
#[error("KKT Incompatibility Error: {}", info)]
|
||||
#[error("KKT Incompatibility Error: {info}")]
|
||||
IncompatibilityError { info: &'static str },
|
||||
|
||||
#[error("KKT Responder Flagged Error: {}", status)]
|
||||
#[error("KKT Responder Flagged Error: {status}")]
|
||||
ResponderFlaggedError { status: KKTStatus },
|
||||
|
||||
#[error("PSQ KEM Error: {}", info)]
|
||||
#[error("PSQ KEM Error: {info}")]
|
||||
KEMError { info: &'static str },
|
||||
|
||||
#[error("Local Function Input Error: {}", info)]
|
||||
#[error("Local Function Input Error: {info}")]
|
||||
FunctionInputError { info: &'static str },
|
||||
|
||||
#[error("{}", info)]
|
||||
#[error("{info}")]
|
||||
X25519Error { info: &'static str },
|
||||
|
||||
#[error("{}", info)]
|
||||
#[error("{info}")]
|
||||
AEADError { info: &'static str },
|
||||
|
||||
#[error("{}", info)]
|
||||
#[error("{info}")]
|
||||
DecodingError { info: &'static str },
|
||||
|
||||
#[error("{}", info)]
|
||||
#[error("{info}")]
|
||||
UnsupportedAlgorithm { info: &'static str },
|
||||
|
||||
#[error("Generic libcrux error")]
|
||||
|
||||
@@ -114,14 +114,14 @@ impl KKTRequestPlaintext {
|
||||
}
|
||||
|
||||
pub(crate) fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN);
|
||||
let mut out = Vec::with_capacity(Self::SIZE);
|
||||
out.extend_from_slice(self.dh_pubkey.as_ref());
|
||||
out.extend_from_slice(self.masked_version_bytes.as_slice());
|
||||
out
|
||||
}
|
||||
|
||||
pub(crate) fn try_from_bytes(b: &[u8]) -> Result<Self, KKTError> {
|
||||
if b.len() != x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN {
|
||||
if b.len() != Self::SIZE {
|
||||
return Err(KKTError::FrameDecodingError {
|
||||
info: "the KKTRequest frame has invalid length".to_string(),
|
||||
});
|
||||
|
||||
@@ -4,15 +4,13 @@
|
||||
//! Post-Quantum Re-Key Protocol
|
||||
|
||||
/// This module implements a stateless post-quantum re-keying protocol in one round-trip.
|
||||
/// We currently support MlKem768 and XWing.
|
||||
/// We currently support MlKem768.
|
||||
///
|
||||
/// This protocol is safe if it runs under a trusted secure channel.
|
||||
///
|
||||
/// Bandwidth costs:
|
||||
/// Request (MlKem768): 1216 bytes
|
||||
/// Response (MlKem768): 1088 bytes
|
||||
/// Request (XWing): 1248 bytes
|
||||
/// Response (XWing): 1120 bytes
|
||||
use libcrux_kem::*;
|
||||
use nym_crypto::hkdf::blake3::derive_key_blake3;
|
||||
use nym_kkt_ciphersuite::{KEM, mceliece, ml_kem768, x25519, xwing};
|
||||
@@ -60,7 +58,7 @@ impl RekeyInitiator {
|
||||
///
|
||||
/// Inputs:
|
||||
/// rng: something that implements CryptoRng + RngCore
|
||||
/// kem: a KEM algorithm (we currently support MlKem768 and XWing)
|
||||
/// kem: a KEM algorithm (we currently support MlKem768 only)
|
||||
///
|
||||
/// Outputs:
|
||||
/// RekeyInitiator: A struct which contains the decapsulation key, the salt and the kem algorithm in use.
|
||||
@@ -171,7 +169,7 @@ where
|
||||
Some(num) => match num {
|
||||
// If message length is 1216 (32 + 1184) then the algorithm should be MlKem768
|
||||
ml_kem768::PUBLIC_KEY_LENGTH => Algorithm::MlKem768,
|
||||
// If message length is 1248 (32 + 1216) then the algorithm should be MlKem768
|
||||
// If message length is 1248 (32 + 1216) then the algorithm should be xwing
|
||||
xwing::PUBLIC_KEY_LENGTH => Algorithm::XWingKemDraft06,
|
||||
// We don't support McEliece because the keys are massive.
|
||||
// If this is a deal-breaker, users can start a new session with PSQ which can use McEliece.
|
||||
|
||||
@@ -146,7 +146,7 @@ impl<'a> KKTResponder<'a> {
|
||||
};
|
||||
|
||||
// for now the response payload is empty
|
||||
let response_payload = Vec::with_capacity(0);
|
||||
let response_payload = Vec::new();
|
||||
|
||||
let frame = KKTFrame::new(local_context, kem_key, response_payload);
|
||||
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
[package]
|
||||
name = "nym-lp"
|
||||
version = "0.1.0"
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme.workspace = true
|
||||
version.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
@@ -11,11 +17,11 @@ bs58 = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
rand09 = { workspace = true }
|
||||
tls_codec = { workspace = true }
|
||||
tls_codec = { workspace = true }
|
||||
tokio = { workspace = true, features = ["net", "io-util"] }
|
||||
|
||||
nym-crypto = { path = "../crypto", features = ["hashing"] }
|
||||
nym-kkt = { path = "../nym-kkt" }
|
||||
nym-crypto = { workspace = true, features = ["hashing"] }
|
||||
nym-kkt = { workspace = true }
|
||||
nym-kkt-ciphersuite = { workspace = true }
|
||||
|
||||
# libcrux dependencies for PSQ (Post-Quantum PSK derivation)
|
||||
@@ -28,7 +34,7 @@ zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
nym-test-utils = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
criterion = { workspace = true, features = ["html_reports"] }
|
||||
nym-test-utils = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Configuration for LP protocol.
|
||||
//!
|
||||
//! LP security stack = KKT (key fetch) → PSQ (PQ PSK) → Noise (transport).
|
||||
//! KEM algorithm selection affects only PSQ layer. Noise always uses X25519 DH.
|
||||
//! Migration to PQ KEMs (MlKem768, XWing) requires only config change.
|
||||
|
||||
use nym_kkt::ciphersuite::KEM;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Default PSK time-to-live (1 hour, matches psk.rs implementation).
|
||||
pub const DEFAULT_PSK_TTL_SECS: u64 = 3600;
|
||||
|
||||
/// Configuration for LP protocol.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LpConfig {
|
||||
/// KEM algorithm for PSQ key encapsulation.
|
||||
/// Supported KEMs: MlKem768, McEliece
|
||||
#[serde(with = "kem_serde")]
|
||||
pub kem_algorithm: KEM,
|
||||
|
||||
/// PSK time-to-live in seconds.
|
||||
pub psk_ttl_secs: u64,
|
||||
|
||||
/// Enable KKT for authenticated key distribution.
|
||||
pub enable_kkt: bool,
|
||||
}
|
||||
|
||||
impl Default for LpConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
kem_algorithm: KEM::MlKem768,
|
||||
psk_ttl_secs: DEFAULT_PSK_TTL_SECS,
|
||||
enable_kkt: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LpConfig {
|
||||
/// Returns PSK TTL as Duration.
|
||||
pub fn psk_ttl(&self) -> Duration {
|
||||
Duration::from_secs(self.psk_ttl_secs)
|
||||
}
|
||||
}
|
||||
|
||||
mod kem_serde {
|
||||
use nym_kkt::ciphersuite::KEM;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
pub fn serialize<S>(kem: &KEM, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match kem {
|
||||
KEM::MlKem768 => "MlKem768",
|
||||
KEM::McEliece => "McEliece",
|
||||
KEM::X25519 => return Err(serde::ser::Error::custom("Unsupported KEM: X25519")),
|
||||
KEM::XWing => return Err(serde::ser::Error::custom("Unsupported KEM: XWing")),
|
||||
}
|
||||
.serialize(serializer)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<KEM, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
match s.as_str() {
|
||||
"MlKem768" => Ok(KEM::MlKem768),
|
||||
"McEliece" => Ok(KEM::McEliece),
|
||||
"X25519" => Err(serde::de::Error::custom("Unsupported KEM: X25519")),
|
||||
"XWing" => Err(serde::de::Error::custom("Unsupported KEM: XWing")),
|
||||
_ => Err(serde::de::Error::custom(format!("Unknown KEM: {}", s))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,10 @@ impl Debug for LpLocalPeer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("LpLocalPeer")
|
||||
.field("ciphersuite", &self.ciphersuite)
|
||||
.field("x25519", &self.x25519.pk)
|
||||
.field(
|
||||
"x25519",
|
||||
&bs58::encode(self.x25519.pk.as_ref()).into_string(),
|
||||
)
|
||||
.field("kem_keypairs", &self.kem_keypairs)
|
||||
.finish()
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ pub struct LpPeerConfig {
|
||||
|
||||
// Determine the hop id.
|
||||
// Should be 0 if node_initiator is true
|
||||
// Should be > 1 if is_exit is true
|
||||
// Should be > 1 && < 16 if is_exit is true
|
||||
hop_id: u8,
|
||||
|
||||
// Determine if the recipient should be an exit node
|
||||
@@ -198,37 +198,36 @@ impl LpPeerConfig {
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> [u8; LP_PEER_CONFIG_SIZE] {
|
||||
let mut output_bytes: [u8; LP_PEER_CONFIG_SIZE] = [0u8; LP_PEER_CONFIG_SIZE];
|
||||
output_bytes[0..4].copy_from_slice(self.pack_config().as_slice());
|
||||
let mut output_bytes = [0u8; LP_PEER_CONFIG_SIZE];
|
||||
output_bytes[0..4].copy_from_slice(&self.pack_config());
|
||||
output_bytes[4..].copy_from_slice(&self.seed);
|
||||
output_bytes
|
||||
}
|
||||
pub fn deserialize(bytes: &[u8]) -> Result<Self, LpError> {
|
||||
if bytes.len() != LP_PEER_CONFIG_SIZE {
|
||||
Err(LpError::DeserializationError(format!(
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Invalid Lp Config Length ({}), expected ({})",
|
||||
bytes.len(),
|
||||
LP_PEER_CONFIG_SIZE
|
||||
)))
|
||||
} else {
|
||||
let (hop_id, is_exit, node_initiator, censorship_resistance) =
|
||||
Self::unpack_first_byte(bytes[0]);
|
||||
|
||||
let mut filler: [u8; FILLER_LEN] = [0u8; FILLER_LEN];
|
||||
filler.copy_from_slice(&bytes[CONFIG_LEN..CONFIG_LEN + FILLER_LEN]);
|
||||
|
||||
let mut seed: [u8; SEED_LEN] = [0u8; SEED_LEN];
|
||||
seed.copy_from_slice(&bytes[CONFIG_LEN + FILLER_LEN..LP_PEER_CONFIG_SIZE]);
|
||||
|
||||
Self::build_checked(
|
||||
hop_id,
|
||||
is_exit,
|
||||
node_initiator,
|
||||
censorship_resistance,
|
||||
seed,
|
||||
filler,
|
||||
)
|
||||
)));
|
||||
}
|
||||
let (hop_id, is_exit, node_initiator, censorship_resistance) =
|
||||
Self::unpack_first_byte(bytes[0]);
|
||||
|
||||
let mut filler = [0u8; FILLER_LEN];
|
||||
filler.copy_from_slice(&bytes[CONFIG_LEN..CONFIG_LEN + FILLER_LEN]);
|
||||
|
||||
let mut seed = [0u8; SEED_LEN];
|
||||
seed.copy_from_slice(&bytes[CONFIG_LEN + FILLER_LEN..LP_PEER_CONFIG_SIZE]);
|
||||
|
||||
Self::build_checked(
|
||||
hop_id,
|
||||
is_exit,
|
||||
node_initiator,
|
||||
censorship_resistance,
|
||||
seed,
|
||||
filler,
|
||||
)
|
||||
}
|
||||
|
||||
fn pack_config(&self) -> [u8; 4] {
|
||||
|
||||
@@ -44,7 +44,7 @@ pub enum LpAction {
|
||||
|
||||
pub type SessionId = [u8; 32];
|
||||
|
||||
/// A session in the Lewes Protocol, handling connection state with Noise.
|
||||
/// A session in the Lewes Protocol..
|
||||
///
|
||||
/// Sessions manage connection state, including LP replay protection.
|
||||
/// Each session has a unique receiving index and sending index for connection identification.
|
||||
|
||||
@@ -17,17 +17,12 @@ use crate::session::{LpAction, LpInput};
|
||||
/// Manages the lifecycle of Lewes Protocol sessions.
|
||||
///
|
||||
/// The SessionManager is responsible for creating, storing, and retrieving sessions
|
||||
#[derive(Default)]
|
||||
pub struct SessionManager {
|
||||
/// Manages state machines directly, keyed by lp_id
|
||||
sessions: HashMap<LpReceiverIndex, LpTransportSession>,
|
||||
}
|
||||
|
||||
impl Default for SessionManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl SessionManager {
|
||||
/// Creates a new session manager with empty session storage.
|
||||
pub fn new() -> Self {
|
||||
|
||||
@@ -79,10 +79,11 @@ async fn read_n_bytes_async_read<R>(reader: &mut R, n: usize) -> Result<Vec<u8>,
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
{
|
||||
let mut buf = vec![0u8; n];
|
||||
if n > MAX_HANDSHAKE_PACKET_SIZE {
|
||||
return Err(LpTransportError::PacketTooBig { size: n });
|
||||
}
|
||||
let mut buf = vec![0u8; n];
|
||||
|
||||
reader
|
||||
.read_exact(&mut buf)
|
||||
.await
|
||||
|
||||
@@ -105,9 +105,9 @@ pub struct GatewayTasksBuilder {
|
||||
|
||||
shutdown_tracker: ShutdownTracker,
|
||||
|
||||
// populated and cached as necessary
|
||||
use_mock_ecash: bool,
|
||||
|
||||
// populated and cached as necessary
|
||||
ecash_manager:
|
||||
Option<Arc<dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync>>,
|
||||
|
||||
@@ -226,7 +226,7 @@ impl GatewayTasksBuilder {
|
||||
> {
|
||||
// Check if we should use mock ecash for testing
|
||||
if self.use_mock_ecash {
|
||||
warn!("Using MockEcashManager for LP testing (credentials NOT verified)");
|
||||
warn!("Using MockEcashManager for testing (credentials NOT verified)");
|
||||
let mock_manager = MockEcashManager::new(Box::new(self.storage.clone()));
|
||||
return Ok(Arc::new(mock_manager)
|
||||
as Arc<
|
||||
|
||||
@@ -127,7 +127,7 @@ impl DailyMerkleTree {
|
||||
pub(crate) fn maybe_rebuild(&mut self) {
|
||||
// every 1000 leaves, rebuild the tree to purge the history
|
||||
// (I wish the API of the library allowed to do it without having to go through those extra steps...)
|
||||
if !self.inserted_leaves.is_empty() && self.inserted_leaves.len() % 1000 == 0 {
|
||||
if !self.inserted_leaves.is_empty() && self.inserted_leaves.len().is_multiple_of(1000) {
|
||||
self.rebuild_without_history();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,7 +803,7 @@ impl EcashState {
|
||||
merkle_entry.maybe_rebuild();
|
||||
|
||||
// toss a coin to check if we should clean memory of old merkle trees
|
||||
if thread_rng().next_u32() % 10000 == 0 {
|
||||
if thread_rng().next_u32().is_multiple_of(10000) {
|
||||
let mut values_to_clean = Vec::new();
|
||||
let cutoff = self.config.ticketbook_retention_cutoff();
|
||||
info!("attempting to remove old issued ticketbooks. the cutoff is set to {cutoff}");
|
||||
|
||||
@@ -172,13 +172,11 @@ pub async fn lp_registration_probe(
|
||||
|
||||
let mut lp_outcome = LpProbeResults::default();
|
||||
|
||||
// Generate Ed25519 keypair for this connection (X25519 will be derived internally by LP)
|
||||
// Generate X25519 keypair for this connection
|
||||
let mut rng09 = rand09::rngs::StdRng::from_os_rng();
|
||||
let client_x25519_keypair = Arc::new(DHKeyPair::new(&mut rng09));
|
||||
|
||||
// Step 0: Derive X25519 keys from Ed25519 for the gateways
|
||||
|
||||
// Create LP registration client (uses Ed25519 keys directly, derives X25519 internally)
|
||||
// Create LP registration client
|
||||
let mut client = LpRegistrationClient::<TcpStream>::new_with_default_config(
|
||||
client_x25519_keypair,
|
||||
peer,
|
||||
@@ -212,16 +210,13 @@ pub async fn lp_registration_probe(
|
||||
let mut rng = rand::thread_rng();
|
||||
let wg_keypair = nym_crypto::asymmetric::x25519::KeyPair::new(&mut rng);
|
||||
|
||||
// Convert gateway identity to ed25519 public key
|
||||
let gateway_ed25519_pubkey = gateway_identity;
|
||||
|
||||
// Register using the new packet-per-connection API (returns GatewayData directly)
|
||||
let ticket_type = TicketType::V1WireguardEntry;
|
||||
let gateway_data = match client
|
||||
.register_dvpn(
|
||||
&mut rng09,
|
||||
&wg_keypair,
|
||||
&gateway_ed25519_pubkey,
|
||||
&gateway_identity,
|
||||
bandwidth_controller,
|
||||
ticket_type,
|
||||
)
|
||||
|
||||
+1
-3
@@ -13,8 +13,6 @@ license = "GPL-3.0"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
@@ -117,7 +115,7 @@ nym-network-requester = { path = "../service-providers/network-requester" }
|
||||
nym-ip-packet-router = { path = "../service-providers/ip-packet-router" }
|
||||
|
||||
# LP dependencies
|
||||
nym-lp = { path = "../common/nym-lp" }
|
||||
nym-lp = { workspace = true }
|
||||
nym-registration-common = { path = "../common/registration" }
|
||||
bincode = { workspace = true }
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ use nym_lp::session::{LpAction, LpInput};
|
||||
use nym_lp::transport::LpHandshakeChannel;
|
||||
use nym_lp::transport::traits::LpTransportChannel;
|
||||
use nym_lp::{LpTransportSession, packet::message::ExpectedResponseSize};
|
||||
use nym_metrics::{add_histogram_obs, inc};
|
||||
use nym_metrics::{add_histogram_obs, inc, inc_by};
|
||||
use nym_registration_common::{LpRegistrationRequest, RegistrationStatus};
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
@@ -625,8 +625,6 @@ where
|
||||
|
||||
/// Emit connection lifecycle metrics
|
||||
fn emit_lifecycle_metrics(&self, graceful: bool) {
|
||||
use nym_metrics::inc_by;
|
||||
|
||||
// Track connection duration
|
||||
let duration = self.stats.start_time.elapsed().as_secs_f64();
|
||||
add_histogram_obs!(
|
||||
|
||||
@@ -453,16 +453,16 @@ impl NymNode {
|
||||
&config.storage_paths.keys.x25519_noise_storage_paths(),
|
||||
)?;
|
||||
|
||||
trace!("attempting to x25519 lp keypair");
|
||||
trace!("attempting to store x25519 lp keypair");
|
||||
store_x25519_lp_keypair(
|
||||
&x25519_lp_keys,
|
||||
&config.storage_paths.keys.x25519_lp_key_paths(),
|
||||
)?;
|
||||
|
||||
trace!("attempting to mlkem768 keypair");
|
||||
trace!("attempting to store mlkem768 keypair");
|
||||
store_mlkem768_keypair(&mlkem, &config.storage_paths.keys.mlkem768_key_paths())?;
|
||||
|
||||
trace!("attempting to mceliece keypair");
|
||||
trace!("attempting to store mceliece keypair");
|
||||
store_mceliece_keypair(&mceliece, &config.storage_paths.keys.mceliece_key_paths())?;
|
||||
|
||||
trace!("creating description file");
|
||||
|
||||
@@ -64,7 +64,6 @@ impl LpBasedRegistrationClient {
|
||||
tracing::debug!("Exit gateway LP address: {exit_address}");
|
||||
|
||||
// Generate fresh x25519 keypairs for LP registration
|
||||
// TODO: persist them for the duration of the sessions
|
||||
let entry_lp_keypair = Arc::new(DHKeyPair::new(&mut rand09::rng()));
|
||||
let exit_lp_keypair = Arc::new(DHKeyPair::new(&mut rand09::rng()));
|
||||
|
||||
@@ -100,7 +99,7 @@ impl LpBasedRegistrationClient {
|
||||
tracing::info!("Registering with exit gateway via entry forwarding");
|
||||
let mut nested_session = NestedLpSession::new(
|
||||
exit_address,
|
||||
exit_lp_keypair,
|
||||
exit_lp_keypair.clone(),
|
||||
exit_peer,
|
||||
exit_ciphersuite,
|
||||
exit_lp_protocol,
|
||||
@@ -153,6 +152,8 @@ impl LpBasedRegistrationClient {
|
||||
Ok(RegistrationResult::Lp(Box::new(LpRegistrationResult {
|
||||
entry_gateway_data,
|
||||
exit_gateway_data,
|
||||
entry_lp_keypair,
|
||||
exit_lp_keypair,
|
||||
bw_controller: self.bandwidth_controller,
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
use super::config::LpRegistrationConfig;
|
||||
use super::error::{LpClientError, Result};
|
||||
use crate::lp_client::helpers::{LpDataDeliverExt, LpDataSendExt};
|
||||
use crate::lp_client::helpers::{LpDataDeliverExt, LpDataSendExt, exponential_backoff_with_jitter};
|
||||
use crate::lp_client::nested_session::connection::NestedConnection;
|
||||
use crate::lp_client::session_helpers::{extract_forwarded_response, prepare_send_packet};
|
||||
use nym_bandwidth_controller::{BandwidthTicketProvider, DEFAULT_TICKETS_TO_SPEND};
|
||||
@@ -23,12 +23,12 @@ use nym_registration_common::{
|
||||
WireguardRegistrationData,
|
||||
};
|
||||
use nym_wireguard_types::PeerPublicKey;
|
||||
use rand09::{CryptoRng, Rng, RngCore};
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::net::TcpStream;
|
||||
use tracing::warn;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
/// LP (Lewes Protocol) registration client for direct gateway connections.
|
||||
///
|
||||
@@ -391,7 +391,6 @@ where
|
||||
let protocol_version = self.gateway_supported_lp_protocol_version;
|
||||
let connection = self.stream_mut()?;
|
||||
|
||||
// TODO:
|
||||
let session = LpTransportSession::psq_handshake_initiator(
|
||||
connection,
|
||||
local_peer,
|
||||
@@ -504,7 +503,7 @@ where
|
||||
/// sends the registration request, and receives the response
|
||||
/// on the same underlying connection.
|
||||
/// Do note that this method does **not** perform retries on network failures,
|
||||
/// for that please use [`Self::register_with_retry`] instead
|
||||
/// for that please use [`Self::handshake_and_register_with_retry`] instead
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `rng` - RNG instance for generating PSK
|
||||
@@ -632,7 +631,7 @@ where
|
||||
/// # Note
|
||||
/// Unlike `register()`, this method handles the full flow including handshake.
|
||||
/// Do NOT call `perform_handshake()` before this method.
|
||||
pub async fn register_with_retry<R>(
|
||||
pub async fn handshake_and_register_with_retry<R>(
|
||||
&mut self,
|
||||
rng: &mut R,
|
||||
wg_keypair: &x25519::KeyPair,
|
||||
@@ -646,59 +645,44 @@ where
|
||||
{
|
||||
tracing::debug!("Starting resilient registration (max_retries={max_retries})",);
|
||||
|
||||
// attempt to perform handshake with retries
|
||||
let mut last_error = None;
|
||||
for attempt in 0..=max_retries {
|
||||
let attempt_display = attempt + 1;
|
||||
debug!("registration attempt {attempt_display}");
|
||||
|
||||
if attempt > 0 {
|
||||
// Exponential backoff with jitter: 100ms, 200ms, 400ms, 800ms, 1600ms (capped)
|
||||
let base_delay_ms = 100u64 * (1 << attempt.min(4));
|
||||
let jitter_ms: u64 = rand09::rng().random_range(0..(base_delay_ms / 4 + 1));
|
||||
let delay = std::time::Duration::from_millis(base_delay_ms + jitter_ms);
|
||||
tracing::info!("Retrying registration (attempt {attempt_display}) after {delay:?}");
|
||||
tokio::time::sleep(delay).await;
|
||||
}
|
||||
|
||||
// Ensure fresh connection and handshake for each attempt
|
||||
// (On retry, the old connection/session may be dead)
|
||||
if self.stream.is_none() || attempt > 0 {
|
||||
// Clear any stale state before re-handshaking
|
||||
self.close();
|
||||
self.transport_session = None;
|
||||
self.close();
|
||||
|
||||
if let Err(e) = self.perform_handshake().await {
|
||||
tracing::warn!("Handshake failed on attempt {attempt_display}: {e}");
|
||||
last_error = Some(e);
|
||||
continue;
|
||||
}
|
||||
exponential_backoff_with_jitter(attempt).await
|
||||
}
|
||||
|
||||
match self
|
||||
.register_dvpn(
|
||||
rng,
|
||||
wg_keypair,
|
||||
gateway_identity,
|
||||
bandwidth_controller,
|
||||
ticket_type,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(data) => {
|
||||
if attempt > 0 {
|
||||
tracing::info!("Registration succeeded on retry attempt {attempt_display}");
|
||||
}
|
||||
return Ok(data);
|
||||
}
|
||||
match self.perform_handshake().await {
|
||||
Ok(_) => break,
|
||||
Err(e) => {
|
||||
tracing::warn!("Registration attempt {attempt_display} failed: {e}");
|
||||
tracing::warn!("Handshake failed on attempt {attempt_display}: {e}");
|
||||
last_error = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(last_error.unwrap_or(LpClientError::RegistrationFailure {
|
||||
message: "Registration failed after all retries".to_string(),
|
||||
}))
|
||||
if self.transport_session.is_none() {
|
||||
return Err(last_error.unwrap_or(LpClientError::RegistrationFailure {
|
||||
message: "Registration failed after all retries".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
self.register_dvpn(
|
||||
rng,
|
||||
wg_keypair,
|
||||
gateway_identity,
|
||||
bandwidth_controller,
|
||||
ticket_type,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| tracing::warn!("Registration failed: {e}"))
|
||||
}
|
||||
|
||||
/// Get the LP session ID (receiver_idx) for this client.
|
||||
|
||||
@@ -11,6 +11,7 @@ use nym_lp::session::{LpAction, LpInput};
|
||||
use nym_registration_common::{
|
||||
LpRegistrationRequest, LpRegistrationResponse, NymNodeLPInformation,
|
||||
};
|
||||
use rand09::Rng;
|
||||
|
||||
pub(crate) trait LpDataSendExt {
|
||||
fn to_lp_data(&self) -> Result<LpMessage, LpClientError>;
|
||||
@@ -83,3 +84,12 @@ pub(crate) fn try_convert_forward_response(action: LpAction) -> Result<Vec<u8>,
|
||||
pub(crate) fn to_lp_remote_peer(data: NymNodeLPInformation) -> LpRemotePeer {
|
||||
LpRemotePeer::new(data.x25519).with_key_digests(data.expected_kem_key_hashes)
|
||||
}
|
||||
|
||||
pub(crate) async fn exponential_backoff_with_jitter(attempt: u32) {
|
||||
// Exponential backoff with jitter: 100ms, 200ms, 400ms, 800ms, 1600ms (capped)
|
||||
let base_delay_ms = 100u64 * (1 << attempt.min(4));
|
||||
let jitter_ms: u64 = rand09::rng().random_range(0..(base_delay_ms / 4 + 1));
|
||||
let delay = std::time::Duration::from_millis(base_delay_ms + jitter_ms);
|
||||
tracing::info!("Retrying registration after the following delay {delay:?}");
|
||||
tokio::time::sleep(delay).await;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
use super::client::LpRegistrationClient;
|
||||
use super::error::{LpClientError, Result};
|
||||
use crate::lp_client::helpers::{LpDataDeliverExt, LpDataSendExt};
|
||||
use crate::lp_client::helpers::{LpDataDeliverExt, LpDataSendExt, exponential_backoff_with_jitter};
|
||||
use crate::lp_client::session_helpers::{extract_forwarded_response, prepare_send_packet};
|
||||
use nym_bandwidth_controller::{BandwidthTicketProvider, DEFAULT_TICKETS_TO_SPEND};
|
||||
use nym_credentials_interface::TicketType;
|
||||
@@ -37,7 +37,7 @@ use nym_registration_common::{
|
||||
WireguardRegistrationData,
|
||||
};
|
||||
use nym_wireguard_types::PeerPublicKey;
|
||||
use rand09::{CryptoRng, Rng, RngCore};
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, warn};
|
||||
@@ -284,7 +284,7 @@ impl NestedLpSession {
|
||||
LpDvpnRegistrationResponseMessageContent::RegistrationFailure(res) => {
|
||||
let reason = res.error;
|
||||
// the registration has failed
|
||||
tracing::warn!("Gateway rejected registration: {reason}");
|
||||
warn!("Gateway rejected registration: {reason}");
|
||||
Err(LpClientError::RegistrationRejected { reason })
|
||||
}
|
||||
LpDvpnRegistrationResponseMessageContent::CompletedRegistration(res) => Ok(res.config),
|
||||
@@ -379,7 +379,7 @@ impl NestedLpSession {
|
||||
LpDvpnRegistrationResponseMessageContent::RegistrationFailure(res) => {
|
||||
let reason = res.error;
|
||||
// the registration has failed
|
||||
tracing::warn!("Gateway rejected registration: {reason}");
|
||||
warn!("Gateway rejected registration: {reason}");
|
||||
return Err(LpClientError::RegistrationRejected { reason });
|
||||
}
|
||||
LpDvpnRegistrationResponseMessageContent::CompletedRegistration(res) => res.config,
|
||||
@@ -434,7 +434,7 @@ impl NestedLpSession {
|
||||
/// - Forwarding through entry gateway fails
|
||||
/// - Response decryption/deserialization fails
|
||||
/// - Gateway rejects the registration
|
||||
pub(crate) async fn handshake_and_register_dvpn<S, R>(
|
||||
pub async fn handshake_and_register_dvpn<S, R>(
|
||||
&mut self,
|
||||
outer_client: &mut LpRegistrationClient<S>,
|
||||
rng: &mut R,
|
||||
@@ -509,60 +509,49 @@ impl NestedLpSession {
|
||||
max_retries
|
||||
);
|
||||
|
||||
// attempt to perform handshake with retries
|
||||
let mut last_error = None;
|
||||
for attempt in 0..=max_retries {
|
||||
if attempt > 0 {
|
||||
// Verify outer session is still usable before retry
|
||||
if !outer_client.is_handshake_complete() {
|
||||
return Err(LpClientError::Other(
|
||||
"Outer session lost during retry - caller must re-establish entry gateway connection".to_string()
|
||||
));
|
||||
}
|
||||
let attempt_display = attempt + 1;
|
||||
debug!("registration attempt {attempt_display}");
|
||||
|
||||
// Exponential backoff with jitter: 100ms, 200ms, 400ms, 800ms, 1600ms (capped)
|
||||
let base_delay_ms = 100u64 * (1 << attempt.min(4));
|
||||
let jitter_ms: u64 = rand09::rng().random_range(0..(base_delay_ms / 4 + 1));
|
||||
let delay = std::time::Duration::from_millis(base_delay_ms + jitter_ms);
|
||||
tracing::info!(
|
||||
"Retrying exit registration (attempt {}) after {:?}",
|
||||
attempt + 1,
|
||||
delay
|
||||
);
|
||||
tokio::time::sleep(delay).await;
|
||||
|
||||
// Clear state machine before retry - handshake needs fresh start
|
||||
self.transport_session = None;
|
||||
// Verify outer session is still usable before retry
|
||||
if !outer_client.is_handshake_complete() {
|
||||
return Err(LpClientError::Other(
|
||||
"Outer session lost during retry - caller must re-establish entry gateway connection".to_string()
|
||||
));
|
||||
}
|
||||
|
||||
match self
|
||||
.handshake_and_register_dvpn(
|
||||
outer_client,
|
||||
rng,
|
||||
wg_keypair,
|
||||
gateway_identity,
|
||||
bandwidth_controller,
|
||||
ticket_type,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(data) => {
|
||||
if attempt > 0 {
|
||||
tracing::info!(
|
||||
"Exit registration succeeded on retry attempt {}",
|
||||
attempt + 1
|
||||
);
|
||||
}
|
||||
return Ok(data);
|
||||
}
|
||||
if attempt > 0 {
|
||||
// Clear state machine before retry - handshake needs fresh start
|
||||
self.transport_session = None;
|
||||
exponential_backoff_with_jitter(attempt).await
|
||||
}
|
||||
|
||||
match self.perform_handshake(outer_client).await {
|
||||
Ok(_) => break,
|
||||
Err(e) => {
|
||||
tracing::warn!("Exit registration attempt {} failed: {}", attempt + 1, e);
|
||||
warn!("Handshake failed on attempt {attempt_display}: {e}");
|
||||
last_error = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(last_error.unwrap_or(LpClientError::RegistrationFailure {
|
||||
message: "Exit Registration failed after all retries".to_string(),
|
||||
}))
|
||||
if self.transport_session.is_none() {
|
||||
return Err(last_error.unwrap_or(LpClientError::RegistrationFailure {
|
||||
message: "Exit Registration failed after all retries".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
self.register_dvpn(
|
||||
outer_client,
|
||||
rng,
|
||||
wg_keypair,
|
||||
gateway_identity,
|
||||
bandwidth_controller,
|
||||
ticket_type,
|
||||
)
|
||||
.await
|
||||
.inspect_err(|e| warn!("Exit Registration failed: {e}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
use nym_authenticator_client::{AuthClientMixnetListenerHandle, AuthenticatorClient};
|
||||
use nym_bandwidth_controller::BandwidthTicketProvider;
|
||||
use nym_lp::peer::DHKeyPair;
|
||||
use nym_registration_common::{AssignedAddresses, WireguardConfiguration};
|
||||
use nym_sdk::mixnet::{EventReceiver, MixnetClient};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub enum RegistrationResult {
|
||||
Mixnet(Box<MixnetRegistrationResult>),
|
||||
@@ -36,6 +38,8 @@ pub struct WireguardRegistrationResult {
|
||||
/// # Fields
|
||||
/// * `entry_gateway_data` - WireGuard configuration from entry gateway
|
||||
/// * `exit_gateway_data` - WireGuard configuration from exit gateway
|
||||
/// * `entry_lp_keypair` - x25519 keypair used on the entry LP channel (persist to resume a pre-established session)
|
||||
/// * `exit_lp_keypair` - x25519 keypair used on the exit LP channel (persist to resume a pre-established session)
|
||||
/// * `bw_controller` - Bandwidth ticket provider for credential management
|
||||
pub struct LpRegistrationResult {
|
||||
/// Gateway configuration data from entry gateway
|
||||
@@ -44,6 +48,14 @@ pub struct LpRegistrationResult {
|
||||
/// Gateway configuration data from exit gateway
|
||||
pub exit_gateway_data: WireguardConfiguration,
|
||||
|
||||
/// x25519 keypair used on the entry channel.
|
||||
/// the purpose of persisting those keys is to be able to resume the pre-established session
|
||||
pub entry_lp_keypair: Arc<DHKeyPair>,
|
||||
|
||||
/// x25519 keypair used on the exit channel
|
||||
/// the purpose of persisting those keys is to be able to resume the pre-established session
|
||||
pub exit_lp_keypair: Arc<DHKeyPair>,
|
||||
|
||||
/// Bandwidth controller for credential management
|
||||
pub bw_controller: Box<dyn BandwidthTicketProvider>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user