KDF and tests
This commit is contained in:
Generated
+16
-26
@@ -2068,20 +2068,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "6.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"hashbrown 0.14.5",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.9.0"
|
||||
@@ -2382,7 +2368,7 @@ dependencies = [
|
||||
"bytecodec",
|
||||
"bytes",
|
||||
"clap",
|
||||
"dashmap 5.5.3",
|
||||
"dashmap",
|
||||
"dirs",
|
||||
"futures",
|
||||
"nym-bin-common",
|
||||
@@ -4871,7 +4857,7 @@ dependencies = [
|
||||
"cw2",
|
||||
"cw3",
|
||||
"cw4",
|
||||
"dashmap 5.5.3",
|
||||
"dashmap",
|
||||
"dotenvy",
|
||||
"futures",
|
||||
"humantime-serde",
|
||||
@@ -5296,7 +5282,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"dashmap 5.5.3",
|
||||
"dashmap",
|
||||
"nym-crypto",
|
||||
"nym-sphinx",
|
||||
"nym-task",
|
||||
@@ -5701,6 +5687,7 @@ dependencies = [
|
||||
"bs58",
|
||||
"cipher",
|
||||
"ctr",
|
||||
"curve25519-dalek",
|
||||
"digest 0.10.7",
|
||||
"ed25519-dalek",
|
||||
"generic-array 0.14.7",
|
||||
@@ -5834,7 +5821,7 @@ dependencies = [
|
||||
"bip39",
|
||||
"bs58",
|
||||
"bytes",
|
||||
"dashmap 5.5.3",
|
||||
"dashmap",
|
||||
"defguard_wireguard_rs",
|
||||
"fastrand 2.3.0",
|
||||
"futures",
|
||||
@@ -6270,7 +6257,8 @@ dependencies = [
|
||||
"bs58",
|
||||
"bytes",
|
||||
"criterion",
|
||||
"dashmap 6.1.0",
|
||||
"dashmap",
|
||||
"nym-crypto",
|
||||
"nym-lp-common",
|
||||
"nym-sphinx",
|
||||
"parking_lot",
|
||||
@@ -6291,7 +6279,7 @@ version = "0.1.0"
|
||||
name = "nym-metrics"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"dashmap 5.5.3",
|
||||
"dashmap",
|
||||
"lazy_static",
|
||||
"prometheus",
|
||||
"tracing",
|
||||
@@ -6301,7 +6289,7 @@ dependencies = [
|
||||
name = "nym-mixnet-client"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"dashmap 5.5.3",
|
||||
"dashmap",
|
||||
"futures",
|
||||
"nym-crypto",
|
||||
"nym-noise",
|
||||
@@ -6400,7 +6388,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"clap",
|
||||
"dashmap 5.5.3",
|
||||
"dashmap",
|
||||
"futures",
|
||||
"log",
|
||||
"nym-bin-common",
|
||||
@@ -6568,7 +6556,7 @@ dependencies = [
|
||||
name = "nym-node-metrics"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"dashmap 5.5.3",
|
||||
"dashmap",
|
||||
"futures",
|
||||
"nym-metrics",
|
||||
"nym-statistics-common",
|
||||
@@ -6890,6 +6878,7 @@ dependencies = [
|
||||
name = "nym-registration-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"nym-authenticator-requests",
|
||||
"nym-credentials-interface",
|
||||
"nym-crypto",
|
||||
@@ -6897,6 +6886,7 @@ dependencies = [
|
||||
"nym-sphinx",
|
||||
"nym-wireguard-types",
|
||||
"serde",
|
||||
"time",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
@@ -6911,7 +6901,7 @@ dependencies = [
|
||||
"bytecodec",
|
||||
"bytes",
|
||||
"clap",
|
||||
"dashmap 5.5.3",
|
||||
"dashmap",
|
||||
"dirs",
|
||||
"dotenvy",
|
||||
"futures",
|
||||
@@ -7190,7 +7180,7 @@ dependencies = [
|
||||
name = "nym-sphinx-chunking"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"dashmap 5.5.3",
|
||||
"dashmap",
|
||||
"log",
|
||||
"nym-crypto",
|
||||
"nym-metrics",
|
||||
@@ -7992,7 +7982,7 @@ checksum = "8b3a2a91fdbfdd4d212c0dcc2ab540de2c2bcbbd90be17de7a7daf8822d010c1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"crossbeam-channel",
|
||||
"dashmap 5.5.3",
|
||||
"dashmap",
|
||||
"fnv",
|
||||
"futures-channel",
|
||||
"futures-executor",
|
||||
|
||||
@@ -207,6 +207,7 @@ aes = "0.8.1"
|
||||
aes-gcm = "0.10.1"
|
||||
aes-gcm-siv = "0.11.1"
|
||||
ammonia = "4"
|
||||
ansi_term = "0.12"
|
||||
anyhow = "1.0.98"
|
||||
arc-swap = "1.7.1"
|
||||
argon2 = "0.5.0"
|
||||
@@ -254,6 +255,7 @@ dirs = "6.0"
|
||||
dotenvy = "0.15.6"
|
||||
dyn-clone = "1.0.19"
|
||||
ecdsa = "0.16"
|
||||
curve25519-dalek = "4.1"
|
||||
ed25519-dalek = "2.1"
|
||||
encoding_rs = "0.8.35"
|
||||
env_logger = "0.11.8"
|
||||
|
||||
@@ -15,6 +15,7 @@ base64.workspace = true
|
||||
bs58 = { workspace = true }
|
||||
blake3 = { workspace = true, features = ["traits-preview"], optional = true }
|
||||
ctr = { workspace = true, optional = true }
|
||||
curve25519-dalek = { workspace = true, optional = true }
|
||||
digest = { workspace = true, optional = true }
|
||||
generic-array = { workspace = true, optional = true }
|
||||
hkdf = { workspace = true, optional = true }
|
||||
@@ -47,7 +48,7 @@ default = []
|
||||
aead = ["dep:aead", "aead/std", "aes-gcm-siv", "generic-array"]
|
||||
naive_jwt = ["asymmetric", "jwt-simple"]
|
||||
serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"]
|
||||
asymmetric = ["x25519-dalek", "ed25519-dalek", "zeroize"]
|
||||
asymmetric = ["x25519-dalek", "ed25519-dalek", "curve25519-dalek", "sha2", "zeroize"]
|
||||
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2"]
|
||||
stream_cipher = ["aes", "ctr", "cipher", "generic-array"]
|
||||
sphinx = ["nym-sphinx-types/sphinx"]
|
||||
|
||||
@@ -213,6 +213,36 @@ impl PublicKey {
|
||||
) -> Result<(), SignatureError> {
|
||||
self.0.verify(message.as_ref(), &signature.0)
|
||||
}
|
||||
|
||||
/// Converts this Ed25519 public key to an X25519 public key for ECDH.
|
||||
///
|
||||
/// Uses the standard ed25519→x25519 conversion by converting the Edwards point
|
||||
/// to Montgomery form. This is the same approach as libsodium's
|
||||
/// `crypto_sign_ed25519_pk_to_curve25519`.
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(x25519::PublicKey)` - The converted X25519 public key
|
||||
/// * `Err(Ed25519RecoveryError)` - If the conversion fails (e.g., low-order point)
|
||||
pub fn to_x25519(&self) -> Result<crate::asymmetric::x25519::PublicKey, Ed25519RecoveryError> {
|
||||
use curve25519_dalek::edwards::CompressedEdwardsY;
|
||||
|
||||
// Decompress the Ed25519 point
|
||||
let compressed = CompressedEdwardsY((*self).to_bytes());
|
||||
let edwards_point = compressed
|
||||
.decompress()
|
||||
.ok_or_else(|| Ed25519RecoveryError::MalformedBytes(
|
||||
SignatureError::from_source("Failed to decompress Ed25519 point".to_string())
|
||||
))?;
|
||||
|
||||
// Convert to Montgomery form
|
||||
let montgomery = edwards_point.to_montgomery();
|
||||
|
||||
// Create X25519 public key
|
||||
crate::asymmetric::x25519::PublicKey::from_bytes(montgomery.as_bytes())
|
||||
.map_err(|_| Ed25519RecoveryError::MalformedBytes(
|
||||
SignatureError::from_source("Failed to convert to X25519".to_string())
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
@@ -334,6 +364,28 @@ impl PrivateKey {
|
||||
let signature_bytes = self.sign(text).to_bytes();
|
||||
bs58::encode(signature_bytes).into_string()
|
||||
}
|
||||
|
||||
/// Converts this Ed25519 private key to an X25519 private key for ECDH.
|
||||
///
|
||||
/// Uses the standard ed25519→x25519 conversion via SHA-512 hash and clamping.
|
||||
/// This is the same approach as libsodium's `crypto_sign_ed25519_sk_to_curve25519`.
|
||||
///
|
||||
/// # Returns
|
||||
/// The converted X25519 private key
|
||||
pub fn to_x25519(&self) -> crate::asymmetric::x25519::PrivateKey {
|
||||
use sha2::{Sha512, Digest};
|
||||
|
||||
// Hash the Ed25519 secret key with SHA-512
|
||||
let hash = Sha512::digest(self.0);
|
||||
|
||||
// Take first 32 bytes (clamping is done automatically by x25519_dalek::StaticSecret)
|
||||
let mut x25519_bytes = [0u8; 32];
|
||||
x25519_bytes.copy_from_slice(&hash[..32]);
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
crate::asymmetric::x25519::PrivateKey::from_bytes(&x25519_bytes)
|
||||
.expect("x25519 key conversion should never fail")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
@@ -517,4 +569,27 @@ mod tests {
|
||||
|
||||
assert_eq!(sig1.to_vec(), sig2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "rand")]
|
||||
fn test_ed25519_to_x25519_ecdh() {
|
||||
let mut rng = thread_rng();
|
||||
|
||||
// Create two ed25519 keypairs
|
||||
let alice_ed = KeyPair::new(&mut rng);
|
||||
let bob_ed = KeyPair::new(&mut rng);
|
||||
|
||||
// Convert to x25519
|
||||
let alice_x25519_private = alice_ed.private_key().to_x25519();
|
||||
let alice_x25519_public = alice_ed.public_key().to_x25519().unwrap();
|
||||
let bob_x25519_private = bob_ed.private_key().to_x25519();
|
||||
let bob_x25519_public = bob_ed.public_key().to_x25519().unwrap();
|
||||
|
||||
// Perform ECDH both ways
|
||||
let alice_shared = alice_x25519_private.diffie_hellman(&bob_x25519_public);
|
||||
let bob_shared = bob_x25519_private.diffie_hellman(&alice_x25519_public);
|
||||
|
||||
// Both should produce the same shared secret
|
||||
assert_eq!(alice_shared, bob_shared);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Key Derivation Functions using Blake3.
|
||||
|
||||
/// Derives a 32-byte key using Blake3's key derivation mode.
|
||||
///
|
||||
/// Uses Blake3's built-in `derive_key` function with domain separation via context string.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `context` - Context string for domain separation (e.g., "nym-lp-psk-v1")
|
||||
/// * `key_material` - Input key material (shared secret from ECDH, etc.)
|
||||
/// * `salt` - Additional salt for freshness (timestamp + nonce)
|
||||
///
|
||||
/// # Returns
|
||||
/// 32-byte derived key suitable for use as PSK
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let psk = derive_key_blake3("nym-lp-psk-v1", shared_secret.as_bytes(), &salt);
|
||||
/// ```
|
||||
pub fn derive_key_blake3(context: &str, key_material: &[u8], salt: &[u8]) -> [u8; 32] {
|
||||
// Concatenate key_material and salt as input
|
||||
let input = [key_material, salt].concat();
|
||||
|
||||
// Use Blake3's derive_key with context for domain separation
|
||||
// blake3::derive_key returns [u8; 32] directly
|
||||
blake3::derive_key(context, &input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_deterministic_derivation() {
|
||||
let context = "test-context";
|
||||
let key_material = b"shared_secret_12345";
|
||||
let salt = b"salt_67890";
|
||||
|
||||
let key1 = derive_key_blake3(context, key_material, salt);
|
||||
let key2 = derive_key_blake3(context, key_material, salt);
|
||||
|
||||
assert_eq!(key1, key2, "Same inputs should produce same output");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_contexts_produce_different_keys() {
|
||||
let key_material = b"shared_secret";
|
||||
let salt = b"salt";
|
||||
|
||||
let key1 = derive_key_blake3("context1", key_material, salt);
|
||||
let key2 = derive_key_blake3("context2", key_material, salt);
|
||||
|
||||
assert_ne!(key1, key2, "Different contexts should produce different keys");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_salts_produce_different_keys() {
|
||||
let context = "test-context";
|
||||
let key_material = b"shared_secret";
|
||||
|
||||
let key1 = derive_key_blake3(context, key_material, b"salt1");
|
||||
let key2 = derive_key_blake3(context, key_material, b"salt2");
|
||||
|
||||
assert_ne!(key1, key2, "Different salts should produce different keys");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_key_material_produces_different_keys() {
|
||||
let context = "test-context";
|
||||
let salt = b"salt";
|
||||
|
||||
let key1 = derive_key_blake3(context, b"secret1", salt);
|
||||
let key2 = derive_key_blake3(context, b"secret2", salt);
|
||||
|
||||
assert_ne!(key1, key2, "Different key material should produce different keys");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_output_length() {
|
||||
let key = derive_key_blake3("test", b"key", b"salt");
|
||||
assert_eq!(key.len(), 32, "Output should be exactly 32 bytes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_inputs() {
|
||||
// Should not panic with empty inputs
|
||||
let key = derive_key_blake3("test", b"", b"");
|
||||
assert_eq!(key.len(), 32);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ pub mod crypto_hash;
|
||||
pub mod hkdf;
|
||||
#[cfg(feature = "hashing")]
|
||||
pub mod hmac;
|
||||
#[cfg(feature = "hashing")]
|
||||
pub mod kdf;
|
||||
#[cfg(all(feature = "asymmetric", feature = "hashing", feature = "stream_cipher"))]
|
||||
pub mod shared_key;
|
||||
pub mod symmetric;
|
||||
|
||||
@@ -21,7 +21,7 @@ byte_string = "1.0"
|
||||
bytes = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
log = { workspace = true }
|
||||
ansi_term = "0.12"
|
||||
ansi_term = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.11"
|
||||
|
||||
@@ -6,22 +6,23 @@ edition = "2021"
|
||||
[dependencies]
|
||||
bincode = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
parking_lot = "0.12"
|
||||
snow = "0.9.6"
|
||||
bs58 = "0.5.1"
|
||||
parking_lot = { workspace = true }
|
||||
snow = { workspace = true }
|
||||
bs58 = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
dashmap = "6.1.0"
|
||||
sha2 = "0.10"
|
||||
ansi_term = "0.12"
|
||||
dashmap = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
ansi_term = { workspace = true }
|
||||
utoipa = { workspace = true, features = ["macros", "non_strict_integers"] }
|
||||
rand = { workspace = true }
|
||||
|
||||
nym-crypto = { path = "../crypto", features = ["hashing", "asymmetric"] }
|
||||
nym-lp-common = { path = "../nym-lp-common" }
|
||||
nym-sphinx = { path = "../nymsphinx" }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
rand = "0.8"
|
||||
rand_chacha = "0.3"
|
||||
|
||||
|
||||
|
||||
@@ -392,4 +392,185 @@ mod tests {
|
||||
// Test multiple packets simulation isn't relevant for datagram parsing
|
||||
// #[test]
|
||||
// fn test_multiple_packets_in_buffer() { ... }
|
||||
|
||||
// === ClientHello Serialization Tests ===
|
||||
|
||||
#[test]
|
||||
fn test_serialize_parse_client_hello() {
|
||||
use crate::message::ClientHelloData;
|
||||
|
||||
let mut dst = BytesMut::new();
|
||||
|
||||
// Create ClientHelloData
|
||||
let client_key = [42u8; 32];
|
||||
let protocol_version = 1u8;
|
||||
let salt = [99u8; 32];
|
||||
let hello_data = ClientHelloData {
|
||||
client_lp_public_key: client_key,
|
||||
protocol_version,
|
||||
salt,
|
||||
};
|
||||
|
||||
// Create a ClientHello message packet
|
||||
let packet = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: 42,
|
||||
counter: 123,
|
||||
},
|
||||
message: LpMessage::ClientHello(hello_data.clone()),
|
||||
trailer: [0; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize the packet
|
||||
serialize_lp_packet(&packet, &mut dst).unwrap();
|
||||
|
||||
// Parse the packet
|
||||
let decoded = parse_lp_packet(&dst).unwrap();
|
||||
|
||||
// Verify the packet fields
|
||||
assert_eq!(decoded.header.protocol_version, 1);
|
||||
assert_eq!(decoded.header.session_id, 42);
|
||||
assert_eq!(decoded.header.counter, 123);
|
||||
|
||||
// Verify message type and data
|
||||
match decoded.message {
|
||||
LpMessage::ClientHello(decoded_data) => {
|
||||
assert_eq!(decoded_data.client_lp_public_key, client_key);
|
||||
assert_eq!(decoded_data.protocol_version, protocol_version);
|
||||
assert_eq!(decoded_data.salt, salt);
|
||||
}
|
||||
_ => panic!("Expected ClientHello message"),
|
||||
}
|
||||
assert_eq!(decoded.trailer, [0; TRAILER_LEN]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_parse_client_hello_with_fresh_salt() {
|
||||
use crate::message::ClientHelloData;
|
||||
|
||||
let mut dst = BytesMut::new();
|
||||
|
||||
// Create ClientHelloData with fresh salt
|
||||
let client_key = [7u8; 32];
|
||||
let hello_data = ClientHelloData::new_with_fresh_salt(client_key, 1);
|
||||
|
||||
// Create a ClientHello message packet
|
||||
let packet = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: 100,
|
||||
counter: 200,
|
||||
},
|
||||
message: LpMessage::ClientHello(hello_data.clone()),
|
||||
trailer: [55; TRAILER_LEN],
|
||||
};
|
||||
|
||||
// Serialize the packet
|
||||
serialize_lp_packet(&packet, &mut dst).unwrap();
|
||||
|
||||
// Parse the packet
|
||||
let decoded = parse_lp_packet(&dst).unwrap();
|
||||
|
||||
// Verify message type and data
|
||||
match decoded.message {
|
||||
LpMessage::ClientHello(decoded_data) => {
|
||||
assert_eq!(decoded_data.client_lp_public_key, client_key);
|
||||
assert_eq!(decoded_data.protocol_version, 1);
|
||||
assert_eq!(decoded_data.salt, hello_data.salt);
|
||||
|
||||
// Verify timestamp can be extracted
|
||||
let timestamp = decoded_data.extract_timestamp();
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
// Timestamp should be within 2 seconds of now
|
||||
assert!((timestamp as i64 - now as i64).abs() <= 2);
|
||||
}
|
||||
_ => panic!("Expected ClientHello message"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_client_hello_malformed_bincode() {
|
||||
// Create a buffer with ClientHello message type but invalid bincode data
|
||||
let mut buf = BytesMut::new();
|
||||
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
|
||||
buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index
|
||||
buf.extend_from_slice(&123u64.to_le_bytes()); // Counter
|
||||
buf.extend_from_slice(&MessageType::ClientHello.to_u16().to_le_bytes()); // ClientHello type
|
||||
|
||||
// Add malformed bincode data (random bytes that won't deserialize to ClientHelloData)
|
||||
buf.extend_from_slice(&[0xFF; 50]); // Invalid bincode data
|
||||
buf.extend_from_slice(&[0; TRAILER_LEN]); // Trailer
|
||||
|
||||
// Attempt to parse
|
||||
let result = parse_lp_packet(&buf);
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(LpError::DeserializationError(_)) => {} // Expected error
|
||||
Err(e) => panic!("Expected DeserializationError, got {:?}", e),
|
||||
Ok(_) => panic!("Expected error, but got Ok"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_client_hello_incomplete_bincode() {
|
||||
// Create a buffer with ClientHello but truncated bincode data
|
||||
let mut buf = BytesMut::new();
|
||||
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
|
||||
buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index
|
||||
buf.extend_from_slice(&123u64.to_le_bytes()); // Counter
|
||||
buf.extend_from_slice(&MessageType::ClientHello.to_u16().to_le_bytes()); // ClientHello type
|
||||
|
||||
// Add incomplete bincode data (only partial ClientHelloData)
|
||||
buf.extend_from_slice(&[0; 20]); // Too few bytes for full ClientHelloData
|
||||
buf.extend_from_slice(&[0; TRAILER_LEN]); // Trailer
|
||||
|
||||
// Attempt to parse
|
||||
let result = parse_lp_packet(&buf);
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(LpError::DeserializationError(_)) => {} // Expected error
|
||||
Err(e) => panic!("Expected DeserializationError, got {:?}", e),
|
||||
Ok(_) => panic!("Expected error, but got Ok"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_hello_different_protocol_versions() {
|
||||
use crate::message::ClientHelloData;
|
||||
|
||||
for version in [0u8, 1, 2, 255] {
|
||||
let mut dst = BytesMut::new();
|
||||
|
||||
let hello_data = ClientHelloData {
|
||||
client_lp_public_key: [version; 32],
|
||||
protocol_version: version,
|
||||
salt: [version.wrapping_add(1); 32],
|
||||
};
|
||||
|
||||
let packet = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: version as u32,
|
||||
counter: version as u64,
|
||||
},
|
||||
message: LpMessage::ClientHello(hello_data.clone()),
|
||||
trailer: [version; TRAILER_LEN],
|
||||
};
|
||||
|
||||
serialize_lp_packet(&packet, &mut dst).unwrap();
|
||||
let decoded = parse_lp_packet(&dst).unwrap();
|
||||
|
||||
match decoded.message {
|
||||
LpMessage::ClientHello(decoded_data) => {
|
||||
assert_eq!(decoded_data.protocol_version, version);
|
||||
assert_eq!(decoded_data.client_lp_public_key, [version; 32]);
|
||||
}
|
||||
_ => panic!("Expected ClientHello message for version {}", version),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ pub mod keypair;
|
||||
pub mod message;
|
||||
pub mod noise_protocol;
|
||||
pub mod packet;
|
||||
pub mod psk;
|
||||
pub mod replay;
|
||||
pub mod session;
|
||||
mod session_integration;
|
||||
@@ -18,6 +19,7 @@ pub use error::LpError;
|
||||
use keypair::PublicKey;
|
||||
pub use message::{ClientHelloData, LpMessage};
|
||||
pub use packet::LpPacket;
|
||||
pub use psk::derive_psk;
|
||||
pub use replay::{ReceivingKeyCounterValidator, ReplayError};
|
||||
pub use session::LpSession;
|
||||
pub use session_manager::SessionManager;
|
||||
@@ -37,21 +39,30 @@ pub fn sessions_for_tests() -> (LpSession, LpSession) {
|
||||
let keypair_2 = Keypair::default();
|
||||
let id = make_lp_id(&keypair_1.public_key(), &keypair_2.public_key());
|
||||
|
||||
// Use consistent salt for deterministic tests
|
||||
let salt = [1u8; 32];
|
||||
|
||||
// Initiator derives PSK from their perspective
|
||||
let initiator_psk = derive_psk(keypair_1.private_key(), &keypair_2.public_key(), &salt);
|
||||
|
||||
let initiator_session = LpSession::new(
|
||||
id,
|
||||
true,
|
||||
&keypair_1.private_key().to_bytes(),
|
||||
&keypair_2.public_key().to_bytes(),
|
||||
&[0u8; 32],
|
||||
&initiator_psk,
|
||||
)
|
||||
.expect("Test session creation failed");
|
||||
|
||||
// Responder derives same PSK from their perspective
|
||||
let responder_psk = derive_psk(keypair_2.private_key(), &keypair_1.public_key(), &salt);
|
||||
|
||||
let responder_session = LpSession::new(
|
||||
id,
|
||||
false,
|
||||
&keypair_2.private_key().to_bytes(),
|
||||
&keypair_1.public_key().to_bytes(),
|
||||
&[0u8; 32],
|
||||
&responder_psk,
|
||||
)
|
||||
.expect("Test session creation failed");
|
||||
|
||||
|
||||
@@ -12,6 +12,54 @@ pub struct ClientHelloData {
|
||||
pub client_lp_public_key: [u8; 32],
|
||||
/// Protocol version for future compatibility
|
||||
pub protocol_version: u8,
|
||||
/// Salt for PSK derivation (32 bytes: 8-byte timestamp + 24-byte nonce)
|
||||
pub salt: [u8; 32],
|
||||
}
|
||||
|
||||
impl ClientHelloData {
|
||||
/// Generates a new ClientHelloData with fresh salt.
|
||||
///
|
||||
/// Salt format: 8 bytes timestamp (u64 LE) + 24 bytes random nonce
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client_lp_public_key` - Client's x25519 public key
|
||||
/// * `protocol_version` - Protocol version number
|
||||
pub fn new_with_fresh_salt(
|
||||
client_lp_public_key: [u8; 32],
|
||||
protocol_version: u8,
|
||||
) -> Self {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
// Generate salt: timestamp + nonce
|
||||
let mut salt = [0u8; 32];
|
||||
|
||||
// First 8 bytes: current timestamp as u64 little-endian
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("System time before UNIX epoch")
|
||||
.as_secs();
|
||||
salt[..8].copy_from_slice(×tamp.to_le_bytes());
|
||||
|
||||
// Last 24 bytes: random nonce
|
||||
use rand::RngCore;
|
||||
rand::thread_rng().fill_bytes(&mut salt[8..]);
|
||||
|
||||
Self {
|
||||
client_lp_public_key,
|
||||
protocol_version,
|
||||
salt,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the timestamp from the salt.
|
||||
///
|
||||
/// # Returns
|
||||
/// Unix timestamp in seconds
|
||||
pub fn extract_timestamp(&self) -> u64 {
|
||||
let mut timestamp_bytes = [0u8; 8];
|
||||
timestamp_bytes.copy_from_slice(&self.salt[..8]);
|
||||
u64::from_le_bytes(timestamp_bytes)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
@@ -87,7 +135,7 @@ impl LpMessage {
|
||||
LpMessage::Busy => 0,
|
||||
LpMessage::Handshake(payload) => payload.len(),
|
||||
LpMessage::EncryptedData(payload) => payload.len(),
|
||||
LpMessage::ClientHello(_) => 33, // 32 bytes key + 1 byte version
|
||||
LpMessage::ClientHello(_) => 65, // 32 bytes key + 1 byte version + 32 bytes salt
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,4 +203,47 @@ mod tests {
|
||||
_ => panic!("Wrong message type"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_hello_salt_generation() {
|
||||
let client_key = [1u8; 32];
|
||||
let hello1 = ClientHelloData::new_with_fresh_salt(client_key, 1);
|
||||
let hello2 = ClientHelloData::new_with_fresh_salt(client_key, 1);
|
||||
|
||||
// Different salts should be generated
|
||||
assert_ne!(hello1.salt, hello2.salt);
|
||||
|
||||
// But timestamps should be very close (within 1 second)
|
||||
let ts1 = hello1.extract_timestamp();
|
||||
let ts2 = hello2.extract_timestamp();
|
||||
assert!((ts1 as i64 - ts2 as i64).abs() <= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_hello_timestamp_extraction() {
|
||||
let client_key = [2u8; 32];
|
||||
let hello = ClientHelloData::new_with_fresh_salt(client_key, 1);
|
||||
|
||||
let timestamp = hello.extract_timestamp();
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
// Timestamp should be within 1 second of now
|
||||
assert!((timestamp as i64 - now as i64).abs() <= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_hello_salt_format() {
|
||||
let client_key = [3u8; 32];
|
||||
let hello = ClientHelloData::new_with_fresh_salt(client_key, 1);
|
||||
|
||||
// First 8 bytes should be non-zero timestamp
|
||||
let timestamp_bytes = &hello.salt[..8];
|
||||
assert_ne!(timestamp_bytes, &[0u8; 8]);
|
||||
|
||||
// Salt should be 32 bytes total
|
||||
assert_eq!(hello.salt.len(), 32);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! PSK (Pre-Shared Key) derivation for LP sessions using Blake3 KDF.
|
||||
//!
|
||||
//! This module implements identity-bound PSK derivation where both client and gateway
|
||||
//! derive the same PSK from their LP keypairs using ECDH + Blake3 KDF.
|
||||
|
||||
use crate::keypair::{PrivateKey, PublicKey};
|
||||
|
||||
/// Context string for Blake3 KDF domain separation.
|
||||
const PSK_CONTEXT: &str = "nym-lp-psk-v1";
|
||||
|
||||
/// Derives a PSK using Blake3 KDF from local private key, remote public key, and salt.
|
||||
///
|
||||
/// # Formula
|
||||
/// ```text
|
||||
/// shared_secret = ECDH(local_private, remote_public)
|
||||
/// psk = Blake3_derive_key(context="nym-lp-psk-v1", input=shared_secret || salt)
|
||||
/// ```
|
||||
///
|
||||
/// # Properties
|
||||
/// - **Identity-bound**: PSK is tied to the LP keypairs of both parties
|
||||
/// - **Session-specific**: Different salts produce different PSKs
|
||||
/// - **Symmetric**: Both sides derive the same PSK from their respective keys
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `local_private` - This side's LP private key
|
||||
/// * `remote_public` - Peer's LP public key
|
||||
/// * `salt` - 32-byte salt (timestamp + nonce from ClientHello)
|
||||
///
|
||||
/// # Returns
|
||||
/// 32-byte PSK suitable for Noise protocol
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// // Client side
|
||||
/// let client_private = client_keypair.private_key();
|
||||
/// let gateway_public = gateway_keypair.public_key();
|
||||
/// let salt = ClientHelloData::new_with_fresh_salt(...).salt;
|
||||
/// let psk = derive_psk(&client_private, &gateway_public, &salt);
|
||||
///
|
||||
/// // Gateway side (derives same PSK)
|
||||
/// let gateway_private = gateway_keypair.private_key();
|
||||
/// let client_public = /* from ClientHello */;
|
||||
/// let psk = derive_psk(&gateway_private, &client_public, &salt);
|
||||
/// ```
|
||||
pub fn derive_psk(
|
||||
local_private: &PrivateKey,
|
||||
remote_public: &PublicKey,
|
||||
salt: &[u8; 32],
|
||||
) -> [u8; 32] {
|
||||
// Perform ECDH to get shared secret
|
||||
let shared_secret = local_private.diffie_hellman(remote_public);
|
||||
|
||||
// Derive PSK using Blake3 KDF with domain separation
|
||||
nym_crypto::kdf::derive_key_blake3(PSK_CONTEXT, shared_secret.as_bytes(), salt)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::keypair::Keypair;
|
||||
|
||||
#[test]
|
||||
fn test_psk_derivation_is_deterministic() {
|
||||
let keypair_1 = Keypair::default();
|
||||
let keypair_2 = Keypair::default();
|
||||
let salt = [1u8; 32];
|
||||
|
||||
// Derive PSK twice with same inputs
|
||||
let psk1 = derive_psk(
|
||||
keypair_1.private_key(),
|
||||
&keypair_2.public_key(),
|
||||
&salt,
|
||||
);
|
||||
let psk2 = derive_psk(
|
||||
keypair_1.private_key(),
|
||||
&keypair_2.public_key(),
|
||||
&salt,
|
||||
);
|
||||
|
||||
assert_eq!(psk1, psk2, "Same inputs should produce same PSK");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psk_derivation_is_symmetric() {
|
||||
let keypair_1 = Keypair::default();
|
||||
let keypair_2 = Keypair::default();
|
||||
let salt = [2u8; 32];
|
||||
|
||||
// Client derives PSK
|
||||
let client_psk = derive_psk(
|
||||
keypair_1.private_key(),
|
||||
&keypair_2.public_key(),
|
||||
&salt,
|
||||
);
|
||||
|
||||
// Gateway derives PSK from their perspective
|
||||
let gateway_psk = derive_psk(
|
||||
keypair_2.private_key(),
|
||||
&keypair_1.public_key(),
|
||||
&salt,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
client_psk, gateway_psk,
|
||||
"Both sides should derive identical PSK"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_salts_produce_different_psks() {
|
||||
let keypair_1 = Keypair::default();
|
||||
let keypair_2 = Keypair::default();
|
||||
|
||||
let salt1 = [1u8; 32];
|
||||
let salt2 = [2u8; 32];
|
||||
|
||||
let psk1 = derive_psk(keypair_1.private_key(), &keypair_2.public_key(), &salt1);
|
||||
let psk2 = derive_psk(keypair_1.private_key(), &keypair_2.public_key(), &salt2);
|
||||
|
||||
assert_ne!(psk1, psk2, "Different salts should produce different PSKs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_keys_produce_different_psks() {
|
||||
let keypair_1 = Keypair::default();
|
||||
let keypair_2 = Keypair::default();
|
||||
let keypair_3 = Keypair::default();
|
||||
let salt = [3u8; 32];
|
||||
|
||||
let psk1 = derive_psk(keypair_1.private_key(), &keypair_2.public_key(), &salt);
|
||||
let psk2 = derive_psk(keypair_1.private_key(), &keypair_3.public_key(), &salt);
|
||||
|
||||
assert_ne!(
|
||||
psk1, psk2,
|
||||
"Different remote keys should produce different PSKs"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psk_output_length() {
|
||||
let keypair_1 = Keypair::default();
|
||||
let keypair_2 = Keypair::default();
|
||||
let salt = [4u8; 32];
|
||||
|
||||
let psk = derive_psk(keypair_1.private_key(), &keypair_2.public_key(), &salt);
|
||||
|
||||
assert_eq!(psk.len(), 32, "PSK should be exactly 32 bytes");
|
||||
}
|
||||
}
|
||||
@@ -21,3 +21,7 @@ nym-crypto = { path = "../crypto" }
|
||||
nym-ip-packet-requests = { path = "../ip-packet-requests" }
|
||||
nym-sphinx = { path = "../nymsphinx" }
|
||||
nym-wireguard-types = { path = "../wireguard-types" }
|
||||
|
||||
[dev-dependencies]
|
||||
bincode.workspace = true
|
||||
time.workspace = true
|
||||
|
||||
@@ -90,7 +90,7 @@ impl LpRegistrationRequest {
|
||||
pub fn validate_timestamp(&self, max_skew_secs: u64) -> bool {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
(now as i64 - self.timestamp as i64).abs() <= max_skew_secs as i64
|
||||
@@ -124,3 +124,140 @@ impl LpRegistrationResponse {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
// ==================== Helper Functions ====================
|
||||
|
||||
fn create_test_gateway_data() -> GatewayData {
|
||||
use std::net::Ipv6Addr;
|
||||
|
||||
GatewayData {
|
||||
public_key: nym_crypto::asymmetric::x25519::PublicKey::from(nym_sphinx::PublicKey::from([1u8; 32])),
|
||||
private_ipv4: Ipv4Addr::new(10, 0, 0, 1),
|
||||
private_ipv6: Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 1),
|
||||
endpoint: "192.168.1.1:8080".parse().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ==================== LpRegistrationRequest Tests ====================
|
||||
|
||||
// ==================== LpRegistrationResponse Tests ====================
|
||||
|
||||
#[test]
|
||||
fn test_lp_registration_response_success() {
|
||||
let gateway_data = create_test_gateway_data();
|
||||
let session_id = 12345;
|
||||
let allocated_bandwidth = 1_000_000_000;
|
||||
|
||||
let response = LpRegistrationResponse::success(session_id, allocated_bandwidth, gateway_data.clone());
|
||||
|
||||
assert!(response.success);
|
||||
assert!(response.error.is_none());
|
||||
assert!(response.gateway_data.is_some());
|
||||
assert_eq!(response.allocated_bandwidth, allocated_bandwidth);
|
||||
assert_eq!(response.session_id, session_id);
|
||||
|
||||
let returned_gw_data = response.gateway_data.unwrap();
|
||||
assert_eq!(returned_gw_data.public_key, gateway_data.public_key);
|
||||
assert_eq!(returned_gw_data.private_ipv4, gateway_data.private_ipv4);
|
||||
assert_eq!(returned_gw_data.private_ipv6, gateway_data.private_ipv6);
|
||||
assert_eq!(returned_gw_data.endpoint, gateway_data.endpoint);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lp_registration_response_error() {
|
||||
let session_id = 54321;
|
||||
let error_msg = String::from("Insufficient bandwidth");
|
||||
|
||||
let response = LpRegistrationResponse::error(session_id, error_msg.clone());
|
||||
|
||||
assert!(!response.success);
|
||||
assert_eq!(response.error, Some(error_msg));
|
||||
assert!(response.gateway_data.is_none());
|
||||
assert_eq!(response.allocated_bandwidth, 0);
|
||||
assert_eq!(response.session_id, session_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lp_registration_response_serialize_deserialize_success() {
|
||||
let gateway_data = create_test_gateway_data();
|
||||
let original = LpRegistrationResponse::success(999, 5_000_000_000, gateway_data);
|
||||
|
||||
// Serialize
|
||||
let serialized = bincode::serialize(&original).expect("Failed to serialize response");
|
||||
|
||||
// Deserialize
|
||||
let deserialized: LpRegistrationResponse =
|
||||
bincode::deserialize(&serialized).expect("Failed to deserialize response");
|
||||
|
||||
assert_eq!(deserialized.success, original.success);
|
||||
assert_eq!(deserialized.error, original.error);
|
||||
assert_eq!(deserialized.allocated_bandwidth, original.allocated_bandwidth);
|
||||
assert_eq!(deserialized.session_id, original.session_id);
|
||||
assert!(deserialized.gateway_data.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lp_registration_response_serialize_deserialize_error() {
|
||||
let original = LpRegistrationResponse::error(777, String::from("Test error message"));
|
||||
|
||||
// Serialize
|
||||
let serialized = bincode::serialize(&original).expect("Failed to serialize response");
|
||||
|
||||
// Deserialize
|
||||
let deserialized: LpRegistrationResponse =
|
||||
bincode::deserialize(&serialized).expect("Failed to deserialize response");
|
||||
|
||||
assert_eq!(deserialized.success, original.success);
|
||||
assert_eq!(deserialized.error, original.error);
|
||||
assert_eq!(deserialized.allocated_bandwidth, 0);
|
||||
assert_eq!(deserialized.session_id, original.session_id);
|
||||
assert!(deserialized.gateway_data.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lp_registration_response_malformed_deserialize() {
|
||||
// Create invalid bincode data
|
||||
let invalid_data = vec![0xFF; 100];
|
||||
|
||||
// Attempt to deserialize
|
||||
let result: Result<LpRegistrationResponse, _> = bincode::deserialize(&invalid_data);
|
||||
|
||||
assert!(result.is_err(), "Expected deserialization to fail for malformed data");
|
||||
}
|
||||
|
||||
// ==================== RegistrationMode Tests ====================
|
||||
|
||||
#[test]
|
||||
fn test_registration_mode_serialize_dvpn() {
|
||||
let mode = RegistrationMode::Dvpn;
|
||||
|
||||
let serialized = bincode::serialize(&mode).expect("Failed to serialize mode");
|
||||
let deserialized: RegistrationMode =
|
||||
bincode::deserialize(&serialized).expect("Failed to deserialize mode");
|
||||
|
||||
assert!(matches!(deserialized, RegistrationMode::Dvpn));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_registration_mode_serialize_mixnet() {
|
||||
let client_id = [99u8; 32];
|
||||
let mode = RegistrationMode::Mixnet { client_id };
|
||||
|
||||
let serialized = bincode::serialize(&mode).expect("Failed to serialize mode");
|
||||
let deserialized: RegistrationMode =
|
||||
bincode::deserialize(&serialized).expect("Failed to deserialize mode");
|
||||
|
||||
match deserialized {
|
||||
RegistrationMode::Mixnet { client_id: id } => {
|
||||
assert_eq!(id, client_id);
|
||||
}
|
||||
_ => panic!("Expected Mixnet mode"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,845 @@
|
||||
# LP (Lewes Protocol) Deployment Guide
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### System Requirements
|
||||
|
||||
**Minimum:**
|
||||
- CPU: 2 cores (x86_64 or ARM64)
|
||||
- RAM: 4 GB
|
||||
- Network: 100 Mbps
|
||||
- Disk: 20 GB SSD
|
||||
|
||||
**Recommended:**
|
||||
- CPU: 4+ cores with AVX2/NEON support (for SIMD optimizations)
|
||||
- RAM: 8+ GB
|
||||
- Network: 1 Gbps
|
||||
- Disk: 50+ GB NVMe SSD
|
||||
|
||||
### Software Dependencies
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
postgresql \
|
||||
wireguard
|
||||
|
||||
# macOS
|
||||
brew install \
|
||||
postgresql \
|
||||
wireguard-tools
|
||||
```
|
||||
|
||||
## Gateway Setup
|
||||
|
||||
### 1. Enable LP in Configuration
|
||||
|
||||
Edit your gateway configuration file (typically `~/.nym/gateways/<id>/config/config.toml`):
|
||||
|
||||
```toml
|
||||
[lp]
|
||||
# Enable the LP listener
|
||||
enabled = true
|
||||
|
||||
# Bind address (0.0.0.0 for all interfaces, 127.0.0.1 for localhost only)
|
||||
bind_address = "0.0.0.0"
|
||||
|
||||
# Control port for LP handshake and registration
|
||||
control_port = 41264
|
||||
|
||||
# Data port (reserved for future use, not currently used)
|
||||
data_port = 51264
|
||||
|
||||
# Maximum concurrent LP connections
|
||||
# Adjust based on expected load and available memory (~5 KB per connection)
|
||||
max_connections = 10000
|
||||
|
||||
# Timestamp tolerance in seconds
|
||||
# ClientHello messages with timestamps outside this window are rejected
|
||||
# Balance security (smaller window) vs clock skew tolerance (larger window)
|
||||
timestamp_tolerance_secs = 30
|
||||
|
||||
# IMPORTANT: ONLY for testing! Never enable in production
|
||||
use_mock_ecash = false
|
||||
```
|
||||
|
||||
### 2. Network Configuration
|
||||
|
||||
#### Firewall Rules
|
||||
|
||||
```bash
|
||||
# Allow LP control port
|
||||
sudo ufw allow 41264/tcp comment 'Nym LP control port'
|
||||
|
||||
# Optional: Rate limiting using iptables
|
||||
sudo iptables -A INPUT -p tcp --dport 41264 -m state --state NEW \
|
||||
-m recent --set --name LP_CONN_LIMIT
|
||||
|
||||
sudo iptables -A INPUT -p tcp --dport 41264 -m state --state NEW \
|
||||
-m recent --update --seconds 60 --hitcount 100 --name LP_CONN_LIMIT \
|
||||
-j DROP
|
||||
```
|
||||
|
||||
#### NAT/Port Forwarding
|
||||
|
||||
If your gateway is behind NAT, forward port 41264:
|
||||
|
||||
```bash
|
||||
# Example for router at 192.168.1.1
|
||||
# Forward external:41264 -> internal:41264 (TCP)
|
||||
|
||||
# Verify with:
|
||||
nc -zv <your-public-ip> 41264
|
||||
```
|
||||
|
||||
### 3. LP Keypair Generation
|
||||
|
||||
LP uses separate keypairs from the gateway's main identity. Generate on first run:
|
||||
|
||||
```bash
|
||||
# Start gateway (will auto-generate LP keypair if missing)
|
||||
./nym-node run --mode gateway --id <gateway-id>
|
||||
|
||||
# LP keypair stored at:
|
||||
# ~/.nym/gateways/<id>/keys/lp_x25519.pem
|
||||
```
|
||||
|
||||
**Key Storage Security:**
|
||||
|
||||
```bash
|
||||
# Restrict key file permissions
|
||||
chmod 600 ~/.nym/gateways/<id>/keys/lp_x25519.pem
|
||||
|
||||
# Backup keys securely (encrypted)
|
||||
gpg -c ~/.nym/gateways/<id>/keys/lp_x25519.pem
|
||||
# Store lp_x25519.pem.gpg in secure location
|
||||
```
|
||||
|
||||
### 4. Database Configuration
|
||||
|
||||
LP requires PostgreSQL for credential tracking:
|
||||
|
||||
```bash
|
||||
# Create database
|
||||
sudo -u postgres createdb nym_gateway
|
||||
|
||||
# Create user
|
||||
sudo -u postgres psql -c "CREATE USER nym_gateway WITH PASSWORD 'strong_password';"
|
||||
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE nym_gateway TO nym_gateway;"
|
||||
|
||||
# Configure in gateway config
|
||||
[storage]
|
||||
database_url = "postgresql://nym_gateway:strong_password@localhost/nym_gateway"
|
||||
```
|
||||
|
||||
**Database Maintenance:**
|
||||
|
||||
```sql
|
||||
-- Index for nullifier lookups (critical for performance)
|
||||
CREATE INDEX idx_nullifiers ON spent_credentials(nullifier);
|
||||
|
||||
-- Periodic cleanup of old nullifiers (run daily via cron)
|
||||
DELETE FROM spent_credentials WHERE expiry < NOW() - INTERVAL '30 days';
|
||||
|
||||
-- Vacuum to reclaim space
|
||||
VACUUM ANALYZE spent_credentials;
|
||||
```
|
||||
|
||||
### 5. WireGuard Configuration (for dVPN mode)
|
||||
|
||||
```bash
|
||||
# Enable WireGuard kernel module
|
||||
sudo modprobe wireguard
|
||||
|
||||
# Verify loaded
|
||||
lsmod | grep wireguard
|
||||
|
||||
# Generate gateway WireGuard keys
|
||||
wg genkey | tee wg_private.key | wg pubkey > wg_public.key
|
||||
chmod 600 wg_private.key
|
||||
|
||||
# Configure in gateway config
|
||||
[wireguard]
|
||||
enabled = true
|
||||
private_key_path = "/path/to/wg_private.key"
|
||||
listen_port = 51820
|
||||
interface_name = "wg-nym"
|
||||
subnet = "10.0.0.0/8"
|
||||
```
|
||||
|
||||
**WireGuard Interface Setup:**
|
||||
|
||||
```bash
|
||||
# Create interface
|
||||
sudo ip link add dev wg-nym type wireguard
|
||||
|
||||
# Configure interface
|
||||
sudo ip addr add 10.0.0.1/8 dev wg-nym
|
||||
sudo ip link set wg-nym up
|
||||
|
||||
# Enable IP forwarding
|
||||
sudo sysctl -w net.ipv4.ip_forward=1
|
||||
echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.conf
|
||||
|
||||
# NAT for WireGuard clients
|
||||
sudo iptables -t nat -A POSTROUTING -s 10.0.0.0/8 -o eth0 -j MASQUERADE
|
||||
```
|
||||
|
||||
### 6. Monitoring Setup
|
||||
|
||||
#### Prometheus Metrics
|
||||
|
||||
LP exposes metrics on the gateway's metrics endpoint (default: `:8080/metrics`):
|
||||
|
||||
```yaml
|
||||
# prometheus.yml
|
||||
scrape_configs:
|
||||
- job_name: 'nym-gateway-lp'
|
||||
static_configs:
|
||||
- targets: ['gateway-host:8080']
|
||||
metric_relabel_configs:
|
||||
# Focus on LP metrics
|
||||
- source_labels: [__name__]
|
||||
regex: 'lp_.*'
|
||||
action: keep
|
||||
```
|
||||
|
||||
**Key Metrics:**
|
||||
|
||||
```promql
|
||||
# Connection metrics
|
||||
nym_gateway_active_lp_connections # Current active connections
|
||||
rate(nym_gateway_lp_connections_total[5m]) # Connection rate
|
||||
rate(nym_gateway_lp_connections_completed_with_error[5m]) # Error rate
|
||||
|
||||
# Handshake metrics
|
||||
rate(nym_gateway_lp_handshakes_success[5m])
|
||||
rate(nym_gateway_lp_handshakes_failed[5m])
|
||||
histogram_quantile(0.95, nym_gateway_lp_handshake_duration_seconds)
|
||||
|
||||
# Registration metrics
|
||||
rate(nym_gateway_lp_registration_success_total[5m])
|
||||
rate(nym_gateway_lp_registration_failed_total[5m])
|
||||
histogram_quantile(0.95, nym_gateway_lp_registration_duration_seconds)
|
||||
|
||||
# Credential metrics
|
||||
rate(nym_gateway_lp_credential_verification_failed[5m])
|
||||
nym_gateway_lp_bandwidth_allocated_bytes_total
|
||||
|
||||
# Error metrics
|
||||
rate(nym_gateway_lp_errors_handshake[5m])
|
||||
rate(nym_gateway_lp_errors_timestamp_too_old[5m])
|
||||
rate(nym_gateway_lp_errors_wg_peer_registration[5m])
|
||||
```
|
||||
|
||||
#### Grafana Dashboard
|
||||
|
||||
Import dashboard JSON (create and export after setup):
|
||||
|
||||
```json
|
||||
{
|
||||
"dashboard": {
|
||||
"title": "Nym Gateway - LP Protocol",
|
||||
"panels": [
|
||||
{
|
||||
"title": "Active Connections",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "nym_gateway_active_lp_connections"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Registration Success Rate",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "rate(nym_gateway_lp_registration_success_total[5m]) / (rate(nym_gateway_lp_registration_success_total[5m]) + rate(nym_gateway_lp_registration_failed_total[5m]))"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Alert Rules
|
||||
|
||||
```yaml
|
||||
# alerting_rules.yml
|
||||
groups:
|
||||
- name: lp_alerts
|
||||
interval: 30s
|
||||
rules:
|
||||
# High connection rejection rate
|
||||
- alert: LPHighRejectionRate
|
||||
expr: rate(nym_gateway_lp_connections_completed_with_error[5m]) > 10
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "High LP connection rejection rate"
|
||||
description: "Gateway {{ $labels.instance }} rejecting {{ $value }} connections/sec"
|
||||
|
||||
# Handshake failure rate > 5%
|
||||
- alert: LPHandshakeFailures
|
||||
expr: |
|
||||
rate(nym_gateway_lp_handshakes_failed[5m]) /
|
||||
(rate(nym_gateway_lp_handshakes_success[5m]) + rate(nym_gateway_lp_handshakes_failed[5m]))
|
||||
> 0.05
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "High LP handshake failure rate"
|
||||
|
||||
# Credential verification issues
|
||||
- alert: LPCredentialVerificationFailures
|
||||
expr: rate(nym_gateway_lp_credential_verification_failed[5m]) > 50
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "High credential verification failure rate"
|
||||
|
||||
# High latency
|
||||
- alert: LPHighLatency
|
||||
expr: histogram_quantile(0.95, nym_gateway_lp_registration_duration_seconds) > 5
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "LP registration latency is high"
|
||||
```
|
||||
|
||||
## Client Configuration
|
||||
|
||||
### 1. Obtain Gateway LP Public Key
|
||||
|
||||
```bash
|
||||
# Query gateway descriptor
|
||||
curl https://validator.nymtech.net/api/v1/gateways/<gateway-identity>
|
||||
|
||||
# Extract LP public key from response
|
||||
{
|
||||
"gateway": {
|
||||
"identity_key": "...",
|
||||
"lp_public_key": "base64-encoded-x25519-public-key",
|
||||
"host": "1.2.3.4",
|
||||
"lp_port": 41264
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Initialize Registration Client
|
||||
|
||||
```rust
|
||||
use nym_registration_client::{RegistrationClient, RegistrationMode};
|
||||
|
||||
// Create client
|
||||
let mut client = RegistrationClient::builder()
|
||||
.gateway_identity("gateway-identity-key")
|
||||
.gateway_lp_public_key(gateway_lp_pubkey)
|
||||
.gateway_lp_address("1.2.3.4:41264")
|
||||
.mode(RegistrationMode::Lp)
|
||||
.build()?;
|
||||
|
||||
// Perform registration
|
||||
let result = client.register_lp(
|
||||
credential, // E-cash credential
|
||||
RegistrationMode::Dvpn {
|
||||
wg_public_key: client_wg_pubkey,
|
||||
}
|
||||
).await?;
|
||||
|
||||
match result {
|
||||
LpRegistrationResult::Success { gateway_data, .. } => {
|
||||
// Use gateway_data to configure WireGuard tunnel
|
||||
}
|
||||
LpRegistrationResult::Error { code, message } => {
|
||||
eprintln!("Registration failed: {}", message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Local Testing Environment
|
||||
|
||||
#### 1. Start Mock Gateway
|
||||
|
||||
```bash
|
||||
# Use mock e-cash verifier (accepts any credential)
|
||||
export LP_USE_MOCK_ECASH=true
|
||||
|
||||
# Start gateway in dev mode
|
||||
./nym-node run --mode gateway --id test-gateway
|
||||
```
|
||||
|
||||
#### 2. Test LP Connection
|
||||
|
||||
```bash
|
||||
# Test TCP connectivity
|
||||
nc -zv localhost 41264
|
||||
|
||||
# Test with openssl (basic TLS check - won't work as LP uses Noise)
|
||||
timeout 5 openssl s_client -connect localhost:41264 < /dev/null
|
||||
# Expected: Connection closes (Noise != TLS)
|
||||
```
|
||||
|
||||
#### 3. Run Integration Tests
|
||||
|
||||
```bash
|
||||
# Run full LP registration test suite
|
||||
cargo test --test lp_integration -- --nocapture
|
||||
|
||||
# Run specific test
|
||||
cargo test --test lp_integration test_dvpn_registration_success
|
||||
```
|
||||
|
||||
### Production Testing
|
||||
|
||||
#### Health Check Script
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# lp_health_check.sh
|
||||
|
||||
GATEWAY_HOST="${1:-localhost}"
|
||||
GATEWAY_PORT="${2:-41264}"
|
||||
|
||||
# Check TCP connectivity
|
||||
if ! timeout 5 nc -zv "$GATEWAY_HOST" "$GATEWAY_PORT" 2>&1 | grep -q succeeded; then
|
||||
echo "CRITICAL: Cannot connect to LP port $GATEWAY_PORT"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Check metrics endpoint
|
||||
ACTIVE_CONNS=$(curl -s "http://$GATEWAY_HOST:8080/metrics" | \
|
||||
grep "^nym_gateway_active_lp_connections" | awk '{print $2}')
|
||||
|
||||
if [ -z "$ACTIVE_CONNS" ]; then
|
||||
echo "WARNING: Cannot read metrics"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "OK: LP listener responding, $ACTIVE_CONNS active connections"
|
||||
exit 0
|
||||
```
|
||||
|
||||
#### Load Testing
|
||||
|
||||
```bash
|
||||
# Install tool
|
||||
cargo install --git https://github.com/nymtech/nym tools/nym-lp-load-test
|
||||
|
||||
# Run load test (1000 concurrent registrations)
|
||||
nym-lp-load-test \
|
||||
--gateway "1.2.3.4:41264" \
|
||||
--gateway-pubkey "base64-key" \
|
||||
--concurrent 1000 \
|
||||
--duration 60s
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Refused
|
||||
|
||||
**Symptom:** `Connection refused` when connecting to port 41264
|
||||
|
||||
**Diagnosis:**
|
||||
```bash
|
||||
# Check if LP listener is running
|
||||
sudo netstat -tlnp | grep 41264
|
||||
|
||||
# Check gateway logs
|
||||
journalctl -u nym-gateway -f | grep LP
|
||||
|
||||
# Check firewall
|
||||
sudo ufw status | grep 41264
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
1. Ensure `lp.enabled = true` in config
|
||||
2. Check bind address (`0.0.0.0` vs `127.0.0.1`)
|
||||
3. Open firewall port: `sudo ufw allow 41264/tcp`
|
||||
4. Restart gateway after config changes
|
||||
|
||||
### Handshake Failures
|
||||
|
||||
**Symptom:** `lp_handshakes_failed` metric increasing
|
||||
|
||||
**Diagnosis:**
|
||||
```bash
|
||||
# Check error logs
|
||||
journalctl -u nym-gateway | grep "LP.*handshake.*failed"
|
||||
|
||||
# Common errors:
|
||||
# - "Noise decryption error" → Wrong keys or MITM
|
||||
# - "Timestamp too old" → Clock skew > 30s
|
||||
# - "Replay detected" → Duplicate connection attempt
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
1. **Noise errors**: Verify client has correct gateway LP public key
|
||||
2. **Timestamp errors**: Sync clocks with NTP
|
||||
```bash
|
||||
sudo timedatectl set-ntp true
|
||||
sudo timedatectl status
|
||||
```
|
||||
3. **Replay errors**: Check for connection retry logic creating duplicates
|
||||
|
||||
### Credential Verification Failures
|
||||
|
||||
**Symptom:** `lp_credential_verification_failed` metric high
|
||||
|
||||
**Diagnosis:**
|
||||
```bash
|
||||
# Check database connectivity
|
||||
psql -U nym_gateway -d nym_gateway -c "SELECT COUNT(*) FROM spent_credentials;"
|
||||
|
||||
# Check ecash manager logs
|
||||
journalctl -u nym-gateway | grep -i credential
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
1. **Database errors**: Check PostgreSQL is running and accessible
|
||||
2. **Signature errors**: Verify ecash contract address is correct
|
||||
3. **Expired credentials**: Client needs to obtain fresh credentials
|
||||
4. **Nullifier collision**: Credential already used (check `spent_credentials` table)
|
||||
|
||||
### High Latency
|
||||
|
||||
**Symptom:** `lp_registration_duration_seconds` p95 > 5 seconds
|
||||
|
||||
**Diagnosis:**
|
||||
```bash
|
||||
# Check database query performance
|
||||
psql -U nym_gateway -d nym_gateway -c "EXPLAIN ANALYZE SELECT * FROM spent_credentials WHERE nullifier = 'test';"
|
||||
|
||||
# Check system load
|
||||
top -bn1 | head -20
|
||||
iostat -x 1 5
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
1. **Database slow**: Add index on nullifier column
|
||||
```sql
|
||||
CREATE INDEX CONCURRENTLY idx_nullifiers ON spent_credentials(nullifier);
|
||||
```
|
||||
2. **CPU bound**: Check if SIMD is enabled
|
||||
```bash
|
||||
# Check for AVX2 support
|
||||
grep avx2 /proc/cpuinfo
|
||||
# Rebuild with target-cpu=native
|
||||
RUSTFLAGS="-C target-cpu=native" cargo build --release
|
||||
```
|
||||
3. **Network latency**: Check RTT to gateway
|
||||
```bash
|
||||
ping -c 10 gateway-host
|
||||
mtr gateway-host
|
||||
```
|
||||
|
||||
### Connection Limit Reached
|
||||
|
||||
**Symptom:** `lp_connections_completed_with_error` high, logs show "connection limit exceeded"
|
||||
|
||||
**Diagnosis:**
|
||||
```bash
|
||||
# Check active connections
|
||||
curl -s http://localhost:8080/metrics | grep active_lp_connections
|
||||
|
||||
# Check system limits
|
||||
ulimit -n # File descriptors per process
|
||||
sysctl net.ipv4.ip_local_port_range
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
1. **Increase max_connections** in config:
|
||||
```toml
|
||||
[lp]
|
||||
max_connections = 20000 # Increased from 10000
|
||||
```
|
||||
2. **Increase system limits**:
|
||||
```bash
|
||||
# /etc/security/limits.conf
|
||||
nym-gateway soft nofile 65536
|
||||
nym-gateway hard nofile 65536
|
||||
|
||||
# /etc/sysctl.conf
|
||||
net.ipv4.ip_local_port_range = 1024 65535
|
||||
net.core.somaxconn = 4096
|
||||
|
||||
# Apply
|
||||
sudo sysctl -p
|
||||
```
|
||||
3. **Check for connection leaks**:
|
||||
```bash
|
||||
# Connections in CLOSE_WAIT (indicates app not closing properly)
|
||||
netstat -an | grep 41264 | grep CLOSE_WAIT | wc -l
|
||||
```
|
||||
|
||||
## Performance Tuning
|
||||
|
||||
### TCP Tuning
|
||||
|
||||
```bash
|
||||
# /etc/sysctl.conf - Optimize for many concurrent connections
|
||||
|
||||
# Increase max backlog
|
||||
net.core.somaxconn = 4096
|
||||
net.ipv4.tcp_max_syn_backlog = 8192
|
||||
|
||||
# Faster TCP timeouts
|
||||
net.ipv4.tcp_fin_timeout = 15
|
||||
net.ipv4.tcp_keepalive_time = 300
|
||||
net.ipv4.tcp_keepalive_probes = 5
|
||||
net.ipv4.tcp_keepalive_intvl = 15
|
||||
|
||||
# Optimize buffer sizes
|
||||
net.core.rmem_max = 134217728
|
||||
net.core.wmem_max = 134217728
|
||||
net.ipv4.tcp_rmem = 4096 87380 67108864
|
||||
net.ipv4.tcp_wmem = 4096 65536 67108864
|
||||
|
||||
# Enable TCP Fast Open
|
||||
net.ipv4.tcp_fastopen = 3
|
||||
|
||||
# Apply
|
||||
sudo sysctl -p
|
||||
```
|
||||
|
||||
### SIMD Optimization
|
||||
|
||||
Ensure gateway is built with CPU-specific optimizations:
|
||||
|
||||
```bash
|
||||
# Check current CPU features
|
||||
rustc --print target-features
|
||||
|
||||
# Build with native CPU features (enables AVX2, SSE4, etc.)
|
||||
RUSTFLAGS="-C target-cpu=native" cargo build --release -p nym-node
|
||||
|
||||
# Verify SIMD is used (check binary for AVX2 instructions)
|
||||
objdump -d target/release/nym-node | grep vpmovzxbw | wc -l
|
||||
# Non-zero result means AVX2 is being used
|
||||
```
|
||||
|
||||
### Database Optimization
|
||||
|
||||
```sql
|
||||
-- Analyze query performance
|
||||
EXPLAIN ANALYZE SELECT * FROM spent_credentials WHERE nullifier = 'xyz';
|
||||
|
||||
-- Essential indexes
|
||||
CREATE INDEX CONCURRENTLY idx_spent_credentials_nullifier ON spent_credentials(nullifier);
|
||||
CREATE INDEX CONCURRENTLY idx_spent_credentials_expiry ON spent_credentials(expiry);
|
||||
|
||||
-- Optimize PostgreSQL config (postgresql.conf)
|
||||
-- Adjust based on available RAM
|
||||
shared_buffers = 2GB # 25% of RAM
|
||||
effective_cache_size = 6GB # 75% of RAM
|
||||
maintenance_work_mem = 512MB
|
||||
work_mem = 64MB
|
||||
max_connections = 200
|
||||
|
||||
-- Enable query planning optimizations
|
||||
random_page_cost = 1.1 # SSD-optimized
|
||||
effective_io_concurrency = 200 # SSD-optimized
|
||||
|
||||
-- Restart PostgreSQL after config changes
|
||||
sudo systemctl restart postgresql
|
||||
```
|
||||
|
||||
## Security Hardening
|
||||
|
||||
### 1. Principle of Least Privilege
|
||||
|
||||
```bash
|
||||
# Run gateway as dedicated user (not root)
|
||||
sudo useradd -r -s /bin/false nym-gateway
|
||||
|
||||
# Set file ownership
|
||||
sudo chown -R nym-gateway:nym-gateway /home/nym-gateway/.nym
|
||||
|
||||
# Systemd service with restrictions
|
||||
[Service]
|
||||
User=nym-gateway
|
||||
Group=nym-gateway
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/home/nym-gateway/.nym
|
||||
```
|
||||
|
||||
### 2. TLS for Metrics Endpoint
|
||||
|
||||
```bash
|
||||
# Use reverse proxy (nginx) for metrics
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name metrics.your-gateway.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/metrics.your-gateway.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/metrics.your-gateway.com/privkey.pem;
|
||||
|
||||
location /metrics {
|
||||
proxy_pass http://127.0.0.1:8080/metrics;
|
||||
# Authentication
|
||||
auth_basic "Metrics";
|
||||
auth_basic_user_file /etc/nginx/.htpasswd;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Key Rotation
|
||||
|
||||
```bash
|
||||
# Generate new LP keypair
|
||||
./nym-node generate-lp-keypair --output new_lp_key.pem
|
||||
|
||||
# Atomic key swap (minimizes downtime)
|
||||
# 1. Stop gateway gracefully
|
||||
systemctl stop nym-gateway
|
||||
|
||||
# 2. Backup old key
|
||||
cp ~/.nym/gateways/<id>/keys/lp_x25519.pem ~/.nym/gateways/<id>/keys/lp_x25519.pem.backup
|
||||
|
||||
# 3. Install new key
|
||||
mv new_lp_key.pem ~/.nym/gateways/<id>/keys/lp_x25519.pem
|
||||
chmod 600 ~/.nym/gateways/<id>/keys/lp_x25519.pem
|
||||
|
||||
# 4. Restart gateway
|
||||
systemctl start nym-gateway
|
||||
|
||||
# 5. Update gateway descriptor (publishes new public key)
|
||||
# This happens automatically on restart
|
||||
```
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Regular Tasks
|
||||
|
||||
**Daily:**
|
||||
- Monitor metrics for anomalies
|
||||
- Check error logs for new patterns
|
||||
- Verify disk space for database growth
|
||||
|
||||
**Weekly:**
|
||||
- Vacuum database to reclaim space
|
||||
```sql
|
||||
VACUUM ANALYZE spent_credentials;
|
||||
```
|
||||
- Review and archive old logs
|
||||
```bash
|
||||
journalctl --vacuum-time=7d
|
||||
```
|
||||
|
||||
**Monthly:**
|
||||
- Update dependencies (security patches)
|
||||
```bash
|
||||
cargo update
|
||||
cargo audit
|
||||
cargo build --release
|
||||
```
|
||||
- Backup configuration and keys
|
||||
- Review and update alert thresholds based on traffic patterns
|
||||
|
||||
**Quarterly:**
|
||||
- Key rotation (if security policy requires)
|
||||
- Performance review and capacity planning
|
||||
- Security audit of configuration
|
||||
|
||||
### Backup Procedure
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# backup_lp.sh
|
||||
|
||||
BACKUP_DIR="/backup/nym-gateway/$(date +%Y%m%d)"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# Backup keys
|
||||
cp -r ~/.nym/gateways/<id>/keys "$BACKUP_DIR/"
|
||||
|
||||
# Backup config
|
||||
cp ~/.nym/gateways/<id>/config/config.toml "$BACKUP_DIR/"
|
||||
|
||||
# Backup database
|
||||
pg_dump -U nym_gateway nym_gateway | gzip > "$BACKUP_DIR/database.sql.gz"
|
||||
|
||||
# Encrypt and upload
|
||||
tar -czf - "$BACKUP_DIR" | gpg -c | aws s3 cp - s3://backups/nym-gateway-$(date +%Y%m%d).tar.gz.gpg
|
||||
```
|
||||
|
||||
### Upgrade Procedure
|
||||
|
||||
```bash
|
||||
# 1. Backup current installation
|
||||
./backup_lp.sh
|
||||
|
||||
# 2. Download new version
|
||||
wget https://github.com/nymtech/nym/releases/download/vX.Y.Z/nym-node
|
||||
|
||||
# 3. Stop gateway
|
||||
systemctl stop nym-gateway
|
||||
|
||||
# 4. Replace binary
|
||||
sudo mv nym-node /usr/local/bin/nym-node
|
||||
sudo chmod +x /usr/local/bin/nym-node
|
||||
|
||||
# 5. Run migrations (if any)
|
||||
nym-node migrate --config ~/.nym/gateways/<id>/config/config.toml
|
||||
|
||||
# 6. Start gateway
|
||||
systemctl start nym-gateway
|
||||
|
||||
# 7. Verify
|
||||
curl http://localhost:8080/metrics | grep lp_connections_total
|
||||
journalctl -u nym-gateway -f
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
### Default Ports
|
||||
|
||||
| Port | Protocol | Purpose |
|
||||
|------|----------|---------|
|
||||
| 41264 | TCP | LP control plane (handshake + registration) |
|
||||
| 51264 | Reserved | LP data plane (future use) |
|
||||
| 51820 | UDP | WireGuard (for dVPN mode) |
|
||||
| 8080 | HTTP | Metrics endpoint |
|
||||
|
||||
### File Locations
|
||||
|
||||
| File | Location | Purpose |
|
||||
|------|----------|---------|
|
||||
| Config | `~/.nym/gateways/<id>/config/config.toml` | Main configuration |
|
||||
| LP Private Key | `~/.nym/gateways/<id>/keys/lp_x25519.pem` | LP static private key |
|
||||
| WG Private Key | `~/.nym/gateways/<id>/keys/wg_private.key` | WireGuard private key |
|
||||
| Database | PostgreSQL database | Nullifier tracking |
|
||||
| Logs | `journalctl -u nym-gateway` | System logs |
|
||||
|
||||
### Useful Commands
|
||||
|
||||
```bash
|
||||
# Check LP listener status
|
||||
sudo netstat -tlnp | grep 41264
|
||||
|
||||
# View real-time logs
|
||||
journalctl -u nym-gateway -f | grep LP
|
||||
|
||||
# Query metrics
|
||||
curl -s http://localhost:8080/metrics | grep "^lp_"
|
||||
|
||||
# Check active connections
|
||||
ss -tn sport = :41264 | wc -l
|
||||
|
||||
# Test credential verification
|
||||
psql -U nym_gateway -d nym_gateway -c \
|
||||
"SELECT COUNT(*) FROM spent_credentials WHERE created_at > NOW() - INTERVAL '1 hour';"
|
||||
```
|
||||
@@ -0,0 +1,470 @@
|
||||
# Lewes Protocol (LP) - Fast Gateway Registration
|
||||
|
||||
## What is LP?
|
||||
|
||||
The Lewes Protocol (LP) is a direct TCP-based registration protocol for Nym gateways. It provides an alternative to mixnet-based registration with different trade-offs.
|
||||
|
||||
**Trade-offs:**
|
||||
- **Faster**: Direct TCP connection vs multi-hop mixnet routing (fewer hops = lower latency)
|
||||
- **Less Anonymous**: Client IP visible to gateway (mixnet hides IP)
|
||||
- **More Reliable**: KCP provides ordered delivery with fast retransmission
|
||||
- **Secure**: Noise XKpsk3 provides mutual authentication and forward secrecy
|
||||
|
||||
**Use LP when:**
|
||||
- Fast registration is important
|
||||
- Network anonymity is not required for the registration step
|
||||
- You want reliable, ordered delivery
|
||||
|
||||
**Use mixnet registration when:**
|
||||
- Network-level anonymity is essential
|
||||
- IP address hiding is required
|
||||
- Traffic analysis resistance is critical
|
||||
|
||||
## Quick Start
|
||||
|
||||
### For Gateway Operators
|
||||
|
||||
```bash
|
||||
# 1. Enable LP in gateway config
|
||||
cat >> ~/.nym/gateways/<id>/config/config.toml << EOF
|
||||
[lp]
|
||||
enabled = true
|
||||
bind_address = "0.0.0.0"
|
||||
control_port = 41264
|
||||
max_connections = 10000
|
||||
timestamp_tolerance_secs = 30
|
||||
EOF
|
||||
|
||||
# 2. Open firewall
|
||||
sudo ufw allow 41264/tcp
|
||||
|
||||
# 3. Restart gateway
|
||||
systemctl restart nym-gateway
|
||||
|
||||
# 4. Verify LP listener
|
||||
sudo netstat -tlnp | grep 41264
|
||||
curl http://localhost:8080/metrics | grep lp_connections_total
|
||||
```
|
||||
|
||||
### For Client Developers
|
||||
|
||||
```rust
|
||||
use nym_registration_client::{RegistrationClient, RegistrationMode};
|
||||
|
||||
// Initialize client
|
||||
let client = RegistrationClient::builder()
|
||||
.gateway_identity("gateway-identity-key")
|
||||
.gateway_lp_public_key(gateway_lp_pubkey) // From gateway descriptor
|
||||
.gateway_lp_address("gateway-ip:41264")
|
||||
.mode(RegistrationMode::Lp)
|
||||
.build()?;
|
||||
|
||||
// Register with dVPN mode
|
||||
let result = client.register_lp(
|
||||
credential,
|
||||
RegistrationMode::Dvpn {
|
||||
wg_public_key: client_wg_pubkey,
|
||||
}
|
||||
).await?;
|
||||
|
||||
match result {
|
||||
LpRegistrationResult::Success { gateway_data, bandwidth_allocated, .. } => {
|
||||
// Use gateway_data to configure WireGuard tunnel
|
||||
}
|
||||
LpRegistrationResult::Error { code, message } => {
|
||||
eprintln!("Registration failed: {} (code: {})", message, code);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Application │
|
||||
│ - Registration Request │
|
||||
│ - E-cash Verification │
|
||||
│ - WireGuard Setup │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ LP Layer │
|
||||
│ - Noise XKpsk3 Handshake │
|
||||
│ - Replay Protection (1024 packets) │
|
||||
│ - Counter-based Sequencing │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ KCP Layer │
|
||||
│ - Ordered Delivery │
|
||||
│ - Fast Retransmission │
|
||||
│ - Congestion Control │
|
||||
└─────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────┐
|
||||
│ TCP │
|
||||
│ - Connection-oriented │
|
||||
│ - Byte Stream │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Why This Stack?
|
||||
|
||||
**TCP**: Reliable connection establishment, handles network-level packet loss.
|
||||
|
||||
**KCP**: Application-level reliability optimized for low latency:
|
||||
- Fast retransmit after 2 duplicate ACKs (vs TCP's 3)
|
||||
- Selective acknowledgment (better than TCP's cumulative ACK)
|
||||
- Minimum RTO of 100ms (configurable, vs TCP's typical 200ms+)
|
||||
|
||||
**LP**: Cryptographic security:
|
||||
- **Noise XKpsk3**: Mutual authentication + forward secrecy
|
||||
- **Replay Protection**: 1024-packet sliding window
|
||||
- **Session Isolation**: Each registration has unique crypto state
|
||||
|
||||
**Application**: Credential verification and peer registration logic.
|
||||
|
||||
## Key Features
|
||||
|
||||
### Security
|
||||
|
||||
**Cryptographic Primitives:**
|
||||
- **Noise XKpsk3**: Mutual authentication with PSK
|
||||
- **ChaCha20-Poly1305**: Authenticated encryption
|
||||
- **X25519**: Key exchange
|
||||
- **Blake3**: KDF for PSK derivation
|
||||
|
||||
**Security Properties:**
|
||||
- Mutual authentication (both client and gateway prove identity)
|
||||
- Forward secrecy (past sessions remain secure if keys compromised)
|
||||
- Replay protection (1024-packet sliding window with SIMD optimization)
|
||||
- Timestamp validation (30-second window, configurable)
|
||||
|
||||
### Observability
|
||||
|
||||
**Prometheus metrics** (from `gateway/src/node/lp_listener/mod.rs:4`):
|
||||
- Connection counts and durations
|
||||
- Handshake success/failure rates
|
||||
- Registration outcomes (dVPN vs Mixnet)
|
||||
- Credential verification results
|
||||
- Error categorization
|
||||
- Latency histograms
|
||||
|
||||
### DoS Protection
|
||||
|
||||
From `gateway/src/node/lp_listener/mod.rs`:
|
||||
- **Connection limits**: Configurable max concurrent connections (default: 10,000)
|
||||
- **Timestamp validation**: Rejects messages outside configured window (default: 30s)
|
||||
- **Replay protection**: Prevents packet replay attacks
|
||||
|
||||
## Components
|
||||
|
||||
### Core Modules
|
||||
|
||||
| Module | Path | Purpose |
|
||||
|--------|------|---------|
|
||||
| **nym-lp** | `common/nym-lp/` | Core LP protocol implementation |
|
||||
| **nym-kcp** | `common/nym-kcp/` | KCP reliability protocol |
|
||||
| **lp_listener** | `gateway/src/node/lp_listener/` | Gateway-side LP listener |
|
||||
|
||||
### Key Files
|
||||
|
||||
**Protocol:**
|
||||
- `common/nym-lp/src/noise_protocol.rs` - Noise state machine
|
||||
- `common/nym-lp/src/replay/validator.rs` - Replay protection
|
||||
- `common/nym-lp/src/psk.rs` - PSK derivation
|
||||
- `common/nym-lp/src/session.rs` - LP session management
|
||||
|
||||
**KCP:**
|
||||
- `common/nym-kcp/src/session.rs` - KCP state machine
|
||||
- `common/nym-kcp/src/packet.rs` - KCP packet format
|
||||
|
||||
**Gateway:**
|
||||
- `gateway/src/node/lp_listener/mod.rs` - TCP listener
|
||||
- `gateway/src/node/lp_listener/handler.rs` - Connection handler
|
||||
- `gateway/src/node/lp_listener/handshake.rs` - Noise handshake
|
||||
- `gateway/src/node/lp_listener/registration.rs` - Registration logic
|
||||
|
||||
## Protocol Flow
|
||||
|
||||
### 1. Connection Establishment
|
||||
|
||||
```
|
||||
Client Gateway
|
||||
|--- TCP SYN ------------> |
|
||||
|<-- TCP SYN-ACK --------- |
|
||||
|--- TCP ACK ------------> |
|
||||
```
|
||||
|
||||
Port: 41264 (default, configurable)
|
||||
|
||||
### 2. Session Setup
|
||||
|
||||
```rust
|
||||
// Client generates session parameters
|
||||
let salt = [timestamp (8 bytes) || nonce (24 bytes)];
|
||||
let shared_secret = ECDH(client_lp_private, gateway_lp_public);
|
||||
let psk = Blake3_derive_key("nym-lp-psk-v1", shared_secret, salt);
|
||||
|
||||
// Deterministic session IDs (order-independent)
|
||||
let lp_id = hash(client_pub || 0xCC || gateway_pub) & 0xFFFFFFFF;
|
||||
let kcp_conv = hash(client_pub || 0xFF || gateway_pub) & 0xFFFFFFFF;
|
||||
```
|
||||
|
||||
### 3. Noise Handshake (XKpsk3)
|
||||
|
||||
```
|
||||
Client Gateway
|
||||
|--- e ------------------------>| [1] Client ephemeral
|
||||
|<-- e, ee, s, es -------------| [2] Gateway ephemeral + static
|
||||
|--- s, se, psk -------------->| [3] Client static + PSK
|
||||
[Transport mode established]
|
||||
```
|
||||
|
||||
**Handshake characteristics:**
|
||||
- 3 messages (1.5 round trips minimum)
|
||||
- Cryptographic operations: ECDH, ChaCha20-Poly1305, SHA-256
|
||||
|
||||
### 4. Registration
|
||||
|
||||
```
|
||||
Client Gateway
|
||||
|--- RegistrationRequest ------>| (encrypted)
|
||||
| | [Verify credential]
|
||||
| | [Register WireGuard peer if dVPN]
|
||||
|<-- RegistrationResponse ------| (encrypted)
|
||||
```
|
||||
|
||||
### 5. Connection Close
|
||||
|
||||
After successful registration, connection is closed. LP is registration-only.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Gateway
|
||||
|
||||
```toml
|
||||
# ~/.nym/gateways/<id>/config/config.toml
|
||||
|
||||
[lp]
|
||||
enabled = true
|
||||
bind_address = "0.0.0.0"
|
||||
control_port = 41264
|
||||
data_port = 51264 # Reserved, not currently used
|
||||
max_connections = 10000
|
||||
timestamp_tolerance_secs = 30
|
||||
use_mock_ecash = false # TESTING ONLY!
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
RUST_LOG=nym_gateway::node::lp_listener=debug
|
||||
LP_ENABLED=true
|
||||
LP_CONTROL_PORT=41264
|
||||
LP_MAX_CONNECTIONS=20000
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Key Metrics
|
||||
|
||||
**Connections:**
|
||||
```promql
|
||||
nym_gateway_active_lp_connections
|
||||
rate(nym_gateway_lp_connections_total[5m])
|
||||
rate(nym_gateway_lp_connections_completed_with_error[5m])
|
||||
```
|
||||
|
||||
**Handshakes:**
|
||||
```promql
|
||||
rate(nym_gateway_lp_handshakes_success[5m])
|
||||
rate(nym_gateway_lp_handshakes_failed[5m])
|
||||
histogram_quantile(0.95, nym_gateway_lp_handshake_duration_seconds)
|
||||
```
|
||||
|
||||
**Registrations:**
|
||||
```promql
|
||||
rate(nym_gateway_lp_registration_success_total[5m])
|
||||
rate(nym_gateway_lp_registration_dvpn_success[5m])
|
||||
rate(nym_gateway_lp_registration_mixnet_success[5m])
|
||||
histogram_quantile(0.95, nym_gateway_lp_registration_duration_seconds)
|
||||
```
|
||||
|
||||
### Recommended Alerts
|
||||
|
||||
```yaml
|
||||
- alert: LPHighRejectionRate
|
||||
expr: rate(nym_gateway_lp_connections_completed_with_error[5m]) > 10
|
||||
for: 5m
|
||||
|
||||
- alert: LPHandshakeFailures
|
||||
expr: rate(nym_gateway_lp_handshakes_failed[5m]) / rate(nym_gateway_lp_handshakes_success[5m]) > 0.05
|
||||
for: 10m
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
```bash
|
||||
# Run all LP tests
|
||||
cargo test -p nym-lp
|
||||
cargo test -p nym-kcp
|
||||
|
||||
# Specific suites
|
||||
cargo test -p nym-lp replay
|
||||
cargo test -p nym-kcp session
|
||||
```
|
||||
|
||||
**Test Coverage** (from code):
|
||||
|
||||
| Component | Tests | Focus Areas |
|
||||
|-----------|-------|-------------|
|
||||
| Replay Protection | 14 | Edge cases, concurrency, overflow |
|
||||
| KCP Session | 12 | Out-of-order, retransmit, window |
|
||||
| PSK Derivation | 5 | Determinism, symmetry, salt |
|
||||
| LP Session | 10 | Handshake, encrypt/decrypt |
|
||||
|
||||
### Missing Tests
|
||||
|
||||
- [ ] End-to-end registration flow
|
||||
- [ ] Network failure scenarios
|
||||
- [ ] Credential verification integration
|
||||
- [ ] Load testing (concurrent connections)
|
||||
- [ ] Performance benchmarks
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Refused
|
||||
|
||||
```bash
|
||||
# Check listener
|
||||
sudo netstat -tlnp | grep 41264
|
||||
|
||||
# Check config
|
||||
grep "lp.enabled" ~/.nym/gateways/<id>/config/config.toml
|
||||
|
||||
# Check firewall
|
||||
sudo ufw status | grep 41264
|
||||
```
|
||||
|
||||
### Handshake Failures
|
||||
|
||||
```bash
|
||||
# Check logs
|
||||
journalctl -u nym-gateway | grep "handshake.*failed"
|
||||
|
||||
# Common causes:
|
||||
# - Wrong gateway LP public key
|
||||
# - Clock skew > 30s (check with: timedatectl)
|
||||
# - Replay detection (retry with fresh connection)
|
||||
```
|
||||
|
||||
### High Rejection Rate
|
||||
|
||||
```bash
|
||||
# Check metrics
|
||||
curl http://localhost:8080/metrics | grep lp_connections_completed_with_error
|
||||
|
||||
# Check connection limit
|
||||
curl http://localhost:8080/metrics | grep active_lp_connections
|
||||
```
|
||||
|
||||
See [LP_DEPLOYMENT.md](./LP_DEPLOYMENT.md#troubleshooting) for detailed guide.
|
||||
|
||||
## Security
|
||||
|
||||
### Threat Model
|
||||
|
||||
**Protected Against:**
|
||||
- ✅ Passive eavesdropping (Noise encryption)
|
||||
- ✅ Active MITM (mutual authentication)
|
||||
- ✅ Replay attacks (counter-based validation)
|
||||
- ✅ Packet injection (Poly1305 MAC)
|
||||
- ✅ DoS (connection limits, timestamp validation)
|
||||
|
||||
**Not Protected Against:**
|
||||
- ❌ Network-level traffic analysis (IP visible)
|
||||
- ❌ Gateway compromise (sees registration data)
|
||||
- ⚠️ Per-IP DoS (global limit only, not per-IP)
|
||||
|
||||
**Key Properties:**
|
||||
- **Forward Secrecy**: Past sessions secure if keys compromised
|
||||
- **Mutual Authentication**: Both parties prove identity
|
||||
- **Replay Protection**: 1024-packet sliding window (verified: 144 bytes memory)
|
||||
- **Constant-Time**: Replay checks are branchless (timing-attack resistant)
|
||||
|
||||
See [LP_SECURITY.md](./LP_SECURITY.md) for complete security analysis.
|
||||
|
||||
### Known Limitations
|
||||
|
||||
1. **No network anonymity**: Client IP visible to gateway
|
||||
2. **Not quantum-resistant**: X25519 vulnerable to Shor's algorithm
|
||||
3. **Single-use sessions**: No session resumption
|
||||
4. **No per-IP rate limiting**: Only global connection limit
|
||||
|
||||
## Implementation Status
|
||||
|
||||
### Implemented ✅
|
||||
|
||||
- Noise XKpsk3 handshake
|
||||
- KCP reliability layer
|
||||
- Replay protection (1024-packet window with SIMD)
|
||||
- PSK derivation (ECDH + Blake3)
|
||||
- dVPN and Mixnet registration modes
|
||||
- E-cash credential verification
|
||||
- WireGuard peer management
|
||||
- Prometheus metrics
|
||||
- DoS protection
|
||||
|
||||
### Pending ⏳
|
||||
|
||||
- End-to-end integration tests
|
||||
- Performance benchmarks
|
||||
- External security audit
|
||||
- Client implementation
|
||||
- Gateway probe support
|
||||
- Per-IP rate limiting
|
||||
|
||||
## Documentation
|
||||
|
||||
- **[LP_PROTOCOL.md](./LP_PROTOCOL.md)**: Complete protocol specification
|
||||
- **[LP_DEPLOYMENT.md](./LP_DEPLOYMENT.md)**: Deployment and operations guide
|
||||
- **[LP_SECURITY.md](./LP_SECURITY.md)**: Security analysis and threat model
|
||||
- **[CODEMAP.md](../CODEMAP.md)**: Repository structure
|
||||
|
||||
## Contributing
|
||||
|
||||
### Getting Started
|
||||
|
||||
1. Read [CODEMAP.md](../CODEMAP.md) for repository structure
|
||||
2. Review [LP_PROTOCOL.md](./LP_PROTOCOL.md) for protocol details
|
||||
3. Check [FUNCTION_LEXICON.md](../FUNCTION_LEXICON.md) for API reference
|
||||
|
||||
### Areas Needing Work
|
||||
|
||||
**High Priority:**
|
||||
- Integration tests for end-to-end registration
|
||||
- Performance benchmarks (latency, throughput, concurrent connections)
|
||||
- Per-IP rate limiting
|
||||
- Client-side implementation
|
||||
|
||||
**Medium Priority:**
|
||||
- Gateway probe support
|
||||
- Load testing framework
|
||||
- Fuzzing for packet parsers
|
||||
|
||||
## License
|
||||
|
||||
Same as parent Nym repository.
|
||||
|
||||
## Support
|
||||
|
||||
- **GitHub Issues**: https://github.com/nymtech/nym/issues
|
||||
- **Discord**: https://discord.gg/nym
|
||||
|
||||
---
|
||||
|
||||
**Protocol Version**: 1.0
|
||||
**Status**: Draft (pending security audit and integration tests)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,261 @@
|
||||
# LP Registration Protocol - Technical Walkthrough
|
||||
|
||||
**Branch**: `drazen/lp-reg`
|
||||
**Status**: Implementation complete, testing in progress
|
||||
**Audience**: Engineering team, technical demo
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
LP Registration is a **fast, direct registration protocol** that allows clients to connect to Nym gateways without traversing the mixnet. It's designed primarily for dVPN use cases where users need quick WireGuard peer setup with sub-second latency.
|
||||
|
||||
### Key Characteristics
|
||||
|
||||
| Aspect | LP Registration | Traditional Mixnet Registration |
|
||||
|--------|----------------|--------------------------------|
|
||||
| **Latency** | Sub-second (100ms-1s) | Multi-second (3-10s) |
|
||||
| **Transport** | Direct TCP (port 41264) | Through mixnet layers |
|
||||
| **Reliability** | Guaranteed delivery | Probabilistic delivery |
|
||||
| **Anonymity** | Client IP visible to gateway | Network-level anonymity |
|
||||
| **Use Case** | dVPN, low-latency services | Privacy-critical applications |
|
||||
| **Security** | Noise XKpsk3 + ChaCha20-Poly1305 | Sphinx packet encryption |
|
||||
|
||||
### Protocol Stack
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Application Layer │
|
||||
│ WireGuard Peer Registration (dVPN) / Mixnet Client. │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ LP Registration Layer │
|
||||
│ LpRegistrationRequest / LpRegistrationResponse │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Noise XKpsk3 Protocol Layer │
|
||||
│ ChaCha20-Poly1305 Encryption + Authentication │
|
||||
│ Replay Protection (1024-pkt window) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Transport Layer │
|
||||
│ TCP (length-prefixed packet framing) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### High-Level Component Diagram
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ CLIENT SIDE │
|
||||
├──────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ nym-registration-client (Client Library) │ │
|
||||
│ │ nym-registration-client/src/lp_client/client.rs:39-62 │ │
|
||||
│ │ │ │
|
||||
│ │ • LpRegistrationClient │ │
|
||||
│ │ • TCP connection management │ │
|
||||
│ │ • Packet serialization/framing │ │
|
||||
│ │ • Integration with BandwidthController │ │
|
||||
│ └────────────────────┬────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────────────┴─────────────────────────────────────────┐ │
|
||||
│ │ common/nym-lp (Protocol Library) │ │
|
||||
│ │ common/nym-lp/src/ (multiple modules) │ │
|
||||
│ │ │ │
|
||||
│ │ • LpStateMachine (state_machine.rs:96-420) │ │
|
||||
│ │ • Noise XKpsk3 (noise_protocol.rs:40-88) │ │
|
||||
│ │ • PSK derivation (psk.rs:28-52) │ │
|
||||
│ │ • ReplayValidator (replay/validator.rs:25-125) │ │
|
||||
│ │ • Message types (message.rs, packet.rs) │ │
|
||||
│ └──────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ TCP (port 41264)
|
||||
│ Length-prefixed packets
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ GATEWAY SIDE │
|
||||
├──────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ LpListener (TCP Accept Loop) │ │
|
||||
│ │ gateway/src/node/lp_listener/mod.rs:226-270 │ │
|
||||
│ │ │ │
|
||||
│ │ • Binds to 0.0.0.0:41264 │ │
|
||||
│ │ • Spawns LpConnectionHandler per connection │ │
|
||||
│ │ • Metrics: active_lp_connections │ │
|
||||
│ └────────────────────┬────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────────────▼─────────────────────────────────────────┐ │
|
||||
│ │ LpConnectionHandler (Per-Connection) │ │
|
||||
│ │ gateway/src/node/lp_listener/handler.rs:101-216 │ │
|
||||
│ │ │ │
|
||||
│ │ 1. Receive ClientHello & validate timestamp │ │
|
||||
│ │ 2. Derive PSK from ECDH + salt │ │
|
||||
│ │ 3. Perform Noise handshake │ │
|
||||
│ │ 4. Receive encrypted registration request │ │
|
||||
│ │ 5. Process registration (delegate to registration.rs) │ │
|
||||
│ │ 6. Send encrypted response │ │
|
||||
│ │ 7. Emit metrics & close │ │
|
||||
│ └────────────────────┬─────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────────────▼─────────────────────────────────────────┐ │
|
||||
│ │ Registration Processor (Business Logic) │ │
|
||||
│ │ gateway/src/node/lp_listener/registration.rs:136-288 │ │
|
||||
│ │ │ │
|
||||
│ │ Mode: dVPN Mode: Mixnet │ │
|
||||
│ │ ├─ register_wg_peer() ├─ (skip WireGuard) │ │
|
||||
│ │ ├─ credential_verification() ├─ credential_verification() │ │
|
||||
│ │ └─ return GatewayData └─ return bandwidth only │ │
|
||||
│ └────────┬───────────────────────────────┬─────────────────────┘ │
|
||||
│ │ │ │
|
||||
│ ┌────────▼───────────────────┐ ┌───────▼─────────────────────┐ │
|
||||
│ │ WireGuard Controller │ │ E-cash Verifier │ │
|
||||
│ │ (PeerControlRequest) │ │ (EcashManager trait) │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ • Add/Remove WG peers │ │ • Verify BLS signature │ │
|
||||
│ │ • Manage peer lifecycle │ │ • Check nullifier spent │ │
|
||||
│ │ • Monitor bandwidth usage │ │ • Allocate bandwidth │ │
|
||||
│ └─────────────────────────────┘ └────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ GatewayStorage (Database) │ │
|
||||
│ │ │ │
|
||||
│ │ Tables: │ │
|
||||
│ │ • wireguard_peers (public_key, client_id, ticket_type) │ │
|
||||
│ │ • bandwidth (client_id, available) │ │
|
||||
│ │ • spent_credentials (nullifier, expiry) │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Roadmap
|
||||
|
||||
### ✅ Completed Components
|
||||
|
||||
1. **Protocol Library** (`common/nym-lp/`)
|
||||
- Noise XKpsk3 implementation
|
||||
- PSK derivation (Blake3 KDF)
|
||||
- Replay protection with SIMD optimization
|
||||
- Message types and packet framing
|
||||
|
||||
2. **Gateway Listener** (`gateway/src/node/lp_listener/`)
|
||||
- TCP accept loop with connection limits
|
||||
- Per-connection handler with lifecycle management
|
||||
- dVPN and Mixnet registration modes
|
||||
- Comprehensive metrics
|
||||
|
||||
3. **Client Library** (`nym-registration-client/`)
|
||||
- Connection management with timeouts
|
||||
- Noise handshake as initiator
|
||||
- E-cash credential integration
|
||||
- Error handling and retries
|
||||
|
||||
4. **Testing Tools** (`nym-gateway-probe/`)
|
||||
- LP-only test mode (`--only-lp-registration`)
|
||||
- Mock e-cash mode (`--use-mock-ecash`)
|
||||
- Detailed test results
|
||||
|
||||
|
||||
## Detailed Documentation
|
||||
|
||||
### For Protocol Deep-Dive
|
||||
📄 **[LP_REGISTRATION_SEQUENCES.md](./LP_REGISTRATION_SEQUENCES.md)**
|
||||
- Complete sequence diagrams for all flows
|
||||
- Happy path with byte-level message formats
|
||||
- Error scenarios and recovery paths
|
||||
- Noise handshake details
|
||||
|
||||
### For Architecture Understanding
|
||||
📄 **[LP_REGISTRATION_ARCHITECTURE.md](./LP_REGISTRATION_ARCHITECTURE.md)**
|
||||
- Component interaction diagrams
|
||||
- Data flow through gateway modules
|
||||
- Client-side architecture
|
||||
- State transitions
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Code Navigation
|
||||
|
||||
### Key Entry Points
|
||||
|
||||
| Component | File Path | Description |
|
||||
|-----------|-----------|-------------|
|
||||
| **Gateway Listener** | `gateway/src/node/lp_listener/mod.rs:226` | `LpListener::run()` - main loop |
|
||||
| **Connection Handler** | `gateway/src/node/lp_listener/handler.rs:101` | `LpConnectionHandler::handle()` - per-connection |
|
||||
| **Registration Logic** | `gateway/src/node/lp_listener/registration.rs:136` | `process_registration()` - business logic |
|
||||
| **Client Entry** | `nym-registration-client/src/lp_client/client.rs:39` | `LpRegistrationClient` struct |
|
||||
| **Protocol Core** | `common/nym-lp/src/state_machine.rs:96` | `LpStateMachine` - Noise protocol |
|
||||
| **Probe Test** | `nym-gateway-probe/src/lib.rs:861` | `lp_registration_probe()` - integration test |
|
||||
|
||||
---
|
||||
|
||||
## Metrics and Observability
|
||||
|
||||
### Prometheus Metrics
|
||||
|
||||
**Connection Metrics**:
|
||||
- `lp_connections_total{result="success|error"}` - Counter
|
||||
- `lp_active_lp_connections` - Gauge
|
||||
- `lp_connection_duration_seconds` - Histogram (buckets: 0.01, 0.1, 1, 5, 10, 30)
|
||||
|
||||
**Handshake Metrics**:
|
||||
- `lp_handshakes_success` - Counter
|
||||
- `lp_handshakes_failed{reason="..."}` - Counter
|
||||
- `lp_handshake_duration_seconds` - Histogram
|
||||
|
||||
**Registration Metrics**:
|
||||
- `lp_registration_attempts_total` - Counter
|
||||
- `lp_registration_success_total{mode="dvpn|mixnet"}` - Counter
|
||||
- `lp_registration_failed_total{reason="..."}` - Counter
|
||||
- `lp_registration_duration_seconds` - Histogram
|
||||
|
||||
**Bandwidth Metrics**:
|
||||
- `lp_bandwidth_allocated_bytes_total` - Counter
|
||||
- `lp_credential_verification_success` - Counter
|
||||
- `lp_credential_verification_failed{reason="..."}` - Counter
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Latency Breakdown
|
||||
|
||||
```
|
||||
Total Registration Time: ~221ms (typical)
|
||||
├─ TCP Connect: 10-20ms
|
||||
├─ Noise Handshake: 40-60ms (3 round-trips)
|
||||
│ ├─ ClientHello send: <5ms
|
||||
│ ├─ Msg 1 (-> e): <5ms
|
||||
│ ├─ Msg 2 (<- e,ee,s,es): 20-30ms (crypto ops)
|
||||
│ └─ Msg 3 (-> s,se,psk): 10-20ms
|
||||
├─ Registration Request: 100-150ms
|
||||
│ ├─ Request encrypt & send: <5ms
|
||||
│ ├─ Gateway processing: 90-140ms
|
||||
│ │ ├─ WireGuard peer setup: 20-40ms
|
||||
│ │ ├─ Database operations: 30-50ms
|
||||
│ │ ├─ E-cash verification: 40-60ms (or <1ms with mock)
|
||||
│ │ └─ Response preparation: <5ms
|
||||
│ └─ Response receive & decrypt: <5ms
|
||||
└─ Connection cleanup: <5ms
|
||||
```
|
||||
|
||||
### Resource Usage
|
||||
|
||||
- **Memory per session**: 144 bytes (state machine + replay window)
|
||||
- **Max concurrent connections**: 10,000 (configurable)
|
||||
- **CPU**: Minimal (ChaCha20 is efficient, SIMD optimizations)
|
||||
- **Database**: 3-5 queries per registration (indexed lookups)
|
||||
@@ -0,0 +1,729 @@
|
||||
# LP (Lewes Protocol) Security Considerations
|
||||
|
||||
## Threat Model
|
||||
|
||||
### Attacker Capabilities
|
||||
|
||||
**Network Attacker (Dolev-Yao Model):**
|
||||
- ✅ Can observe all network traffic
|
||||
- ✅ Can inject, modify, drop, or replay packets
|
||||
- ✅ Can perform active MITM attacks
|
||||
- ✅ Cannot break cryptographic primitives (ChaCha20, Poly1305, X25519)
|
||||
- ✅ Cannot forge digital signatures (BLS12-381)
|
||||
|
||||
**Gateway Compromise:**
|
||||
- ✅ Attacker gains full access to gateway server
|
||||
- ✅ Can read all gateway state (keys, credentials, database)
|
||||
- ✅ Can impersonate gateway to clients
|
||||
- ❌ Cannot decrypt past sessions (forward secrecy)
|
||||
- ❌ Cannot impersonate clients without their keys
|
||||
|
||||
**Client Compromise:**
|
||||
- ✅ Attacker gains access to client device
|
||||
- ✅ Can read client LP private key
|
||||
- ✅ Can impersonate client to gateways
|
||||
- ❌ Cannot decrypt other clients' sessions
|
||||
|
||||
### Security Goals
|
||||
|
||||
**Confidentiality:**
|
||||
- Registration requests encrypted end-to-end
|
||||
- E-cash credentials protected from eavesdropping
|
||||
- WireGuard keys transmitted securely
|
||||
|
||||
**Integrity:**
|
||||
- All messages authenticated with Poly1305 MAC
|
||||
- Tampering detected and rejected
|
||||
- Replay attacks prevented
|
||||
|
||||
**Authentication:**
|
||||
- Mutual authentication via Noise XKpsk3
|
||||
- Gateway proves possession of LP private key
|
||||
- Client proves possession of LP private key + PSK
|
||||
|
||||
**Forward Secrecy:**
|
||||
- Compromise of long-term keys doesn't reveal past sessions
|
||||
- Ephemeral keys provide PFS
|
||||
- Session keys destroyed after use
|
||||
|
||||
**Non-Goals:**
|
||||
- **Network anonymity**: LP reveals client IP to gateway (use mixnet for anonymity)
|
||||
- **Traffic analysis resistance**: Packet timing visible to network observer
|
||||
- **Deniability**: Parties can prove who they communicated with
|
||||
|
||||
## Cryptographic Design
|
||||
|
||||
### Noise Protocol XKpsk3
|
||||
|
||||
**Pattern:**
|
||||
```
|
||||
XKpsk3:
|
||||
<- s
|
||||
...
|
||||
-> e
|
||||
<- e, ee, s, es
|
||||
-> s, se, psk
|
||||
```
|
||||
|
||||
**Security Properties:**
|
||||
|
||||
| Property | Provided | Rationale |
|
||||
|----------|----------|-----------|
|
||||
| Confidentiality (forward) | ✅ Strong | Ephemeral keys + PSK |
|
||||
| Confidentiality (backward) | ✅ Weak | PSK compromise affects future |
|
||||
| Authentication (initiator) | ✅ Strong | Static key + PSK |
|
||||
| Authentication (responder) | ✅ Strong | Static key known upfront |
|
||||
| Identity hiding (initiator) | ✅ Yes | Static key encrypted |
|
||||
| Identity hiding (responder) | ❌ No | Static key in handshake msg 2 |
|
||||
|
||||
**Why XKpsk3:**
|
||||
|
||||
1. **Known responder identity**: Client knows gateway's LP public key from descriptor
|
||||
2. **Mutual authentication**: Both sides prove identity
|
||||
3. **PSK binding**: Links session to out-of-band PSK (prevents MITM with compromised static key alone)
|
||||
4. **Forward secrecy**: Ephemeral keys provide PFS even if static keys leaked
|
||||
|
||||
**Alternative patterns considered:**
|
||||
|
||||
- **IKpsk2**: No forward secrecy (rejected)
|
||||
- **XXpsk3**: More round trips, unknown identities (not needed)
|
||||
- **NKpsk0**: No client authentication (rejected)
|
||||
|
||||
### PSK Derivation Security
|
||||
|
||||
**Formula:**
|
||||
```
|
||||
shared_secret = X25519(client_lp_private, gateway_lp_public)
|
||||
psk = Blake3_derive_key("nym-lp-psk-v1", shared_secret, salt)
|
||||
```
|
||||
|
||||
**Security Analysis:**
|
||||
|
||||
1. **ECDH Security**: Based on Curve25519 hardness (128-bit security)
|
||||
- Resistant to quantum attacks up to Grover's algorithm (64-bit post-quantum)
|
||||
- Well-studied, no known vulnerabilities
|
||||
|
||||
2. **Blake3 KDF Security**:
|
||||
- Output indistinguishable from random (PRF security)
|
||||
- Domain separation via context string prevents cross-protocol attacks
|
||||
- Collision resistance: 128 bits (birthday bound on 256-bit hash)
|
||||
|
||||
3. **Salt Freshness**:
|
||||
- Timestamp component prevents long-term PSK reuse
|
||||
- Nonce component provides per-session uniqueness
|
||||
- Both transmitted in ClientHello (integrity protected by timestamp validation + Noise handshake)
|
||||
|
||||
**Attack Scenarios:**
|
||||
|
||||
| Attack | Feasibility | Mitigation |
|
||||
|--------|-------------|------------|
|
||||
| Brute force PSK | ❌ Infeasible | 2^128 operations (Curve25519 DL) |
|
||||
| Quantum attack on ECDH | ⚠️ Future threat | Shor's algorithm breaks X25519 in polynomial time |
|
||||
| Salt replay | ❌ Prevented | Timestamp validation (30s window) |
|
||||
| Cross-protocol PSK reuse | ❌ Prevented | Domain separation ("nym-lp-psk-v1") |
|
||||
|
||||
**Quantum Resistance:**
|
||||
|
||||
LP is **not quantum-resistant** due to X25519 use. Future upgrade path:
|
||||
|
||||
```rust
|
||||
// Hybrid PQ-KEM (future)
|
||||
let classical_secret = X25519(client_priv, gateway_pub);
|
||||
let pq_secret = Kyber768::encaps(gateway_pq_pub);
|
||||
let psk = Blake3_derive_key(
|
||||
"nym-lp-psk-v2-pq",
|
||||
classical_secret || pq_secret,
|
||||
salt
|
||||
);
|
||||
```
|
||||
|
||||
### Replay Protection Analysis
|
||||
|
||||
**Algorithm: Sliding Window with Bitmap**
|
||||
|
||||
```rust
|
||||
Window size: 1024 packets
|
||||
Bitmap: [u64; 16] = 1024 bits
|
||||
|
||||
For counter C:
|
||||
- Accept if C >= next (new packet)
|
||||
- Reject if C + 1024 < next (too old)
|
||||
- Reject if bitmap[C % 1024] == 1 (duplicate)
|
||||
- Otherwise accept and mark
|
||||
```
|
||||
|
||||
**Security Properties:**
|
||||
|
||||
1. **Replay Window**: 1024 packets
|
||||
- Sufficient for expected reordering in TCP+KCP
|
||||
- Small enough to limit replay attack surface
|
||||
|
||||
2. **Memory Efficiency**: 128 bytes bitmap
|
||||
- Tracks 1024 unique counters
|
||||
- O(1) lookup and insertion
|
||||
|
||||
3. **Overflow Handling**: Wraps at u64::MAX
|
||||
- Properly handles counter wraparound
|
||||
- Unlikely to occur (2^64 packets = trillions)
|
||||
|
||||
**Attack Scenarios:**
|
||||
|
||||
| Attack | Feasibility | Mitigation |
|
||||
|--------|-------------|------------|
|
||||
| Replay within window | ❌ Prevented | Bitmap tracking |
|
||||
| Replay outside window | ❌ Prevented | Window boundary check |
|
||||
| Counter overflow | ⚠️ Theoretical | Wraparound handling + 2^64 limit |
|
||||
| Timing attack | ❌ Mitigated | Branchless execution |
|
||||
|
||||
**Timing Attack Resistance:**
|
||||
|
||||
```rust
|
||||
// Constant-time check (branchless)
|
||||
pub fn will_accept_branchless(&self, counter: u64) -> ReplayResult<()> {
|
||||
let is_growing = counter >= self.next;
|
||||
let too_far_back = /* calculated */;
|
||||
let duplicate = self.check_bit_branchless(counter);
|
||||
|
||||
// Single branch at end (constant-time up to this point)
|
||||
let result = if is_growing { Ok(()) }
|
||||
else if too_far_back { Err(OutOfWindow) }
|
||||
else if duplicate { Err(Duplicate) }
|
||||
else { Ok(()) };
|
||||
result.unwrap()
|
||||
}
|
||||
```
|
||||
|
||||
**SIMD Optimizations:**
|
||||
|
||||
- AVX2, SSE2, NEON: SIMD clears are constant-time
|
||||
- Scalar fallback: Also constant-time (no data-dependent branches)
|
||||
- No timing channels revealed through replay check
|
||||
|
||||
## Denial of Service (DoS) Protection
|
||||
|
||||
### Connection-Level DoS
|
||||
|
||||
**Attack:** Flood gateway with TCP connections
|
||||
|
||||
**Mitigations:**
|
||||
|
||||
1. **Max connections limit** (default: 10,000):
|
||||
```rust
|
||||
if active_connections >= max_connections {
|
||||
return; // Drop new connection
|
||||
}
|
||||
```
|
||||
- Prevents memory exhaustion (~5 KB per connection)
|
||||
- Configurable based on gateway capacity
|
||||
|
||||
2. **TCP SYN cookies** (kernel-level):
|
||||
```bash
|
||||
sysctl -w net.ipv4.tcp_syncookies=1
|
||||
```
|
||||
- Prevents SYN flood attacks
|
||||
- No state allocated until 3-way handshake completes
|
||||
|
||||
3. **Connection rate limiting** (iptables):
|
||||
```bash
|
||||
iptables -A INPUT -p tcp --dport 41264 -m state --state NEW \
|
||||
-m recent --update --seconds 60 --hitcount 100 -j DROP
|
||||
```
|
||||
- Limits new connections per IP
|
||||
- 100 connections/minute threshold
|
||||
|
||||
**Residual Risk:**
|
||||
|
||||
- ⚠️ **No per-IP limit in application**: Current implementation only has global limit
|
||||
- **Recommendation**: Add per-IP tracking:
|
||||
```rust
|
||||
let connections_from_ip = ip_tracker.get(remote_addr.ip());
|
||||
if connections_from_ip >= per_ip_limit {
|
||||
return; // Reject
|
||||
}
|
||||
```
|
||||
|
||||
### Handshake-Level DoS
|
||||
|
||||
**Attack:** Start handshakes but never complete them
|
||||
|
||||
**Mitigations:**
|
||||
|
||||
1. **Handshake timeout**: Noise state machine times out
|
||||
- Implementation: Tokio task timeout (implicit)
|
||||
- Recommended: Explicit 15-second timeout
|
||||
|
||||
2. **State cleanup**: Connection dropped if handshake fails
|
||||
```rust
|
||||
if handshake_fails {
|
||||
drop(connection); // Frees memory immediately
|
||||
}
|
||||
```
|
||||
|
||||
3. **No resource allocation before handshake**:
|
||||
- Replay validator created only after handshake
|
||||
- Minimal memory usage during handshake (~200 bytes)
|
||||
|
||||
**Attack Scenarios:**
|
||||
|
||||
| Attack | Resource Consumed | Mitigation |
|
||||
|--------|-------------------|------------|
|
||||
| Half-open connections | TCP state (~4 KB) | SYN cookies |
|
||||
| Incomplete handshakes | Noise state (~200 B) | Timeout + cleanup |
|
||||
| Slow clients | Connection slot | Timeout + max connections |
|
||||
|
||||
### Timestamp-Based DoS
|
||||
|
||||
**Attack:** Replay old ClientHello messages
|
||||
|
||||
**Mitigation:**
|
||||
|
||||
```rust
|
||||
let timestamp_age = now - client_hello.timestamp;
|
||||
if timestamp_age > 30_seconds {
|
||||
return Err(TimestampTooOld);
|
||||
}
|
||||
if timestamp_age < -30_seconds {
|
||||
return Err(TimestampFromFuture);
|
||||
}
|
||||
```
|
||||
|
||||
**Properties:**
|
||||
|
||||
- 30-second window limits replay attack surface
|
||||
- Clock skew tolerance: ±30 seconds (reasonable for NTP)
|
||||
- Metrics track rejections: `lp_timestamp_validation_rejected`
|
||||
|
||||
**Residual Risk:**
|
||||
|
||||
- ⚠️ 30-second window allows replay of ClientHello within window
|
||||
- **Mitigation**: Replay protection on post-handshake messages
|
||||
|
||||
### Credential Verification DoS
|
||||
|
||||
**Attack:** Flood gateway with fake credentials
|
||||
|
||||
**Mitigations:**
|
||||
|
||||
1. **Fast rejection path**:
|
||||
```rust
|
||||
// Check signature before database lookup
|
||||
if !verify_bls_signature(&credential) {
|
||||
return Err(InvalidSignature); // Fast path
|
||||
}
|
||||
// Only then check database
|
||||
```
|
||||
|
||||
2. **Database indexing**:
|
||||
```sql
|
||||
CREATE INDEX idx_nullifiers ON spent_credentials(nullifier);
|
||||
```
|
||||
- O(log n) nullifier lookup instead of O(n)
|
||||
|
||||
3. **Rate limiting** (future):
|
||||
- Limit credential verification attempts per IP
|
||||
- Exponential backoff for repeated failures
|
||||
|
||||
**Performance Impact:**
|
||||
|
||||
- BLS signature verification: ~5ms per credential
|
||||
- Database lookup: ~1ms (with index)
|
||||
- Total: ~6ms per invalid credential
|
||||
|
||||
**Attack Cost:**
|
||||
|
||||
- Attacker must generate BLS signatures (computationally expensive)
|
||||
- Invalid signatures rejected before database query
|
||||
- Real cost is in valid-looking but fake credentials (still requires crypto)
|
||||
|
||||
## Threat Scenarios
|
||||
|
||||
### Scenario 1: Passive Eavesdropper
|
||||
|
||||
**Attacker:** Network observer (ISP, hostile network)
|
||||
|
||||
**Capabilities:**
|
||||
- Observe all LP traffic (including ClientHello)
|
||||
- Analyze packet sizes, timing, patterns
|
||||
|
||||
**Protections:**
|
||||
- ✅ ClientHello metadata visible but not sensitive (timestamp, nonce)
|
||||
- ✅ Noise handshake encrypts all subsequent messages
|
||||
- ✅ Registration request fully encrypted (credential not visible)
|
||||
- ✅ ChaCha20-Poly1305 provides IND-CCA2 security
|
||||
|
||||
**Leakage:**
|
||||
- ⚠️ Client IP address visible (inherent to TCP)
|
||||
- ⚠️ Packet timing reveals registration events
|
||||
- ⚠️ Connection to known gateway suggests Nym usage
|
||||
|
||||
**Recommendation:** Use LP for fast registration, mixnet for anonymity-critical operations.
|
||||
|
||||
### Scenario 2: Active MITM
|
||||
|
||||
**Attacker:** On-path adversary (malicious router, hostile WiFi)
|
||||
|
||||
**Capabilities:**
|
||||
- Intercept, modify, drop, inject packets
|
||||
- Cannot break cryptography
|
||||
|
||||
**Protections:**
|
||||
- ✅ Noise XKpsk3 mutual authentication prevents impersonation
|
||||
- ✅ Client verifies gateway's LP static public key
|
||||
- ✅ Gateway verifies client via PSK derivation
|
||||
- ✅ Any packet modification detected via Poly1305 MAC
|
||||
|
||||
**Attack Attempts:**
|
||||
|
||||
1. **Impersonate Gateway**:
|
||||
- Attacker doesn't have gateway's LP private key
|
||||
- Cannot complete handshake (Noise fails at `es` mix)
|
||||
- Client rejects connection
|
||||
|
||||
2. **Impersonate Client**:
|
||||
- Attacker doesn't know client's LP private key
|
||||
- Cannot derive correct PSK
|
||||
- Noise fails at `psk` mix in message 3
|
||||
- Gateway rejects connection
|
||||
|
||||
3. **Modify Messages**:
|
||||
- Poly1305 MAC fails
|
||||
- Noise decryption fails
|
||||
- Connection aborted
|
||||
|
||||
**Residual Risk:**
|
||||
- ⚠️ DoS possible (drop packets, connection killed)
|
||||
- ✅ Cannot learn registration data or credentials
|
||||
|
||||
### Scenario 3: Gateway Compromise
|
||||
|
||||
**Attacker:** Full access to gateway server
|
||||
|
||||
**Capabilities:**
|
||||
- Read all gateway state (keys, database, memory)
|
||||
- Modify gateway behavior
|
||||
- Impersonate gateway to clients
|
||||
|
||||
**Impact:**
|
||||
|
||||
1. **Current Sessions**: Compromised
|
||||
- Attacker can decrypt ongoing registration requests
|
||||
- Can steal credentials from current sessions
|
||||
|
||||
2. **Past Sessions**: Protected (forward secrecy)
|
||||
- Ephemeral keys already destroyed
|
||||
- Cannot decrypt recorded traffic
|
||||
|
||||
3. **Future Sessions**: Compromised until key rotation
|
||||
- Attacker can impersonate gateway
|
||||
- Can steal credentials from new registrations
|
||||
|
||||
**Mitigations:**
|
||||
|
||||
1. **Key Rotation**:
|
||||
```bash
|
||||
# Generate new LP keypair
|
||||
./nym-node generate-lp-keypair
|
||||
# Update gateway descriptor (automatic on restart)
|
||||
```
|
||||
- Invalidates attacker's stolen keys
|
||||
- Clients fetch new public key from descriptor
|
||||
|
||||
2. **Monitoring**:
|
||||
- Detect anomalous credential verification patterns
|
||||
- Alert on unusual database access
|
||||
- Monitor for key file modifications
|
||||
|
||||
3. **Defense in Depth**:
|
||||
- E-cash credentials have limited value (time-bound, nullifiers)
|
||||
- WireGuard keys rotatable by client
|
||||
- No long-term sensitive data stored
|
||||
|
||||
**Credential Reuse Prevention:**
|
||||
|
||||
- Nullifier stored in database
|
||||
- Nullifier = Hash(credential_data)
|
||||
- Even with database access, attacker cannot create new credentials
|
||||
- Can only steal credentials submitted during compromise window
|
||||
|
||||
### Scenario 4: Replay Attack
|
||||
|
||||
**Attacker:** Records past LP sessions, replays later
|
||||
|
||||
**Attack Attempts:**
|
||||
|
||||
1. **Replay ClientHello**:
|
||||
- Timestamp validation rejects messages > 30s old
|
||||
- Nonce in salt changes per session
|
||||
- Cannot reuse old ClientHello
|
||||
|
||||
2. **Replay Handshake Messages**:
|
||||
- Noise uses ephemeral keys (fresh each session)
|
||||
- Replaying old handshake messages fails (wrong ephemeral key)
|
||||
- Handshake fails, no session established
|
||||
|
||||
3. **Replay Post-Handshake Packets**:
|
||||
- Counter-based replay protection
|
||||
- Bitmap tracks last 1024 packets
|
||||
- Duplicate counters rejected
|
||||
- Cannot replay old encrypted messages
|
||||
|
||||
4. **Replay Entire Session**:
|
||||
- Different ephemeral keys each time
|
||||
- Cannot replay connection to gateway
|
||||
- Even if gateway state reset, timestamp rejects old ClientHello
|
||||
|
||||
**Success Probability:** Negligible (< 2^-128)
|
||||
|
||||
### Scenario 5: Quantum Adversary (Future)
|
||||
|
||||
**Attacker:** Quantum computer with Shor's algorithm
|
||||
|
||||
**Capabilities:**
|
||||
- Break X25519 ECDH in polynomial time
|
||||
- Recover LP static private keys from public keys
|
||||
- Does NOT break symmetric crypto (ChaCha20, Blake3)
|
||||
|
||||
**Impact:**
|
||||
|
||||
1. **Recorded Traffic**: Vulnerable
|
||||
- Attacker records all LP traffic now
|
||||
- Breaks X25519 later with quantum computer
|
||||
- Recovers PSKs from recorded ClientHellos
|
||||
- Decrypts recorded sessions
|
||||
|
||||
2. **Real-Time Interception**: Full compromise
|
||||
- Can impersonate gateway (knows private key)
|
||||
- Can decrypt all traffic
|
||||
- Complete MITM attack
|
||||
|
||||
**Mitigations (Future):**
|
||||
|
||||
1. **Hybrid PQ-KEM**:
|
||||
```rust
|
||||
// Use both classical and post-quantum KEM
|
||||
let classical = X25519(client_priv, gateway_pub);
|
||||
let pq = Kyber768::encaps(gateway_pq_pub);
|
||||
let psk = Blake3(classical || pq, salt);
|
||||
```
|
||||
|
||||
2. **Post-Quantum Noise**:
|
||||
- Noise specification supports PQ KEMs
|
||||
- Can upgrade to Kyber, NTRU, or SIKE
|
||||
- Requires protocol version 2
|
||||
|
||||
**Timeline:**
|
||||
- Quantum threat: ~10-20 years away
|
||||
- PQ upgrade: Can be deployed when threat becomes real
|
||||
- Backward compatibility: Support both classical and PQ
|
||||
|
||||
## Security Recommendations
|
||||
|
||||
### For Gateway Operators
|
||||
|
||||
**High Priority:**
|
||||
|
||||
1. **Enable all DoS protections**:
|
||||
```toml
|
||||
[lp]
|
||||
max_connections = 10000 # Adjust based on capacity
|
||||
timestamp_tolerance_secs = 30 # Don't increase unnecessarily
|
||||
```
|
||||
|
||||
2. **Secure key storage**:
|
||||
```bash
|
||||
chmod 600 ~/.nym/gateways/<id>/keys/lp_x25519.pem
|
||||
# Encrypt disk if possible
|
||||
```
|
||||
|
||||
3. **Monitor metrics**:
|
||||
- Alert on high `lp_handshakes_failed`
|
||||
- Alert on unusual `lp_timestamp_validation_rejected`
|
||||
- Track `lp_credential_verification_failed` patterns
|
||||
|
||||
4. **Keep database secure**:
|
||||
- Regular backups
|
||||
- Index on `nullifier` column
|
||||
- Periodic cleanup of old nullifiers
|
||||
|
||||
**Medium Priority:**
|
||||
|
||||
5. **Implement per-IP rate limiting** (future):
|
||||
```rust
|
||||
const MAX_CONNECTIONS_PER_IP: usize = 10;
|
||||
```
|
||||
|
||||
6. **Regular key rotation**:
|
||||
- Rotate LP keypair every 6-12 months
|
||||
- Coordinate with network updates
|
||||
|
||||
7. **Firewall hardening**:
|
||||
```bash
|
||||
# Only allow LP port
|
||||
ufw default deny incoming
|
||||
ufw allow 41264/tcp
|
||||
```
|
||||
|
||||
### For Client Developers
|
||||
|
||||
**High Priority:**
|
||||
|
||||
1. **Verify gateway LP public key**:
|
||||
```rust
|
||||
// Fetch from trusted source (network descriptor)
|
||||
let gateway_lp_pubkey = fetch_gateway_descriptor(gateway_id)
|
||||
.await?
|
||||
.lp_public_key;
|
||||
|
||||
// Pin for future connections
|
||||
save_pinned_key(gateway_id, gateway_lp_pubkey);
|
||||
```
|
||||
|
||||
2. **Handle errors securely**:
|
||||
```rust
|
||||
match registration_result {
|
||||
Err(LpError::Replay(_)) => {
|
||||
// DO NOT retry immediately (might be replay attack)
|
||||
log::warn!("Replay detected, waiting before retry");
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
Err(e) => {
|
||||
// Other errors safe to retry
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Use fresh credentials**:
|
||||
- Don't reuse credentials across registrations
|
||||
- Check credential expiry before attempting registration
|
||||
|
||||
**Medium Priority:**
|
||||
|
||||
4. **Implement connection timeout**:
|
||||
```rust
|
||||
tokio::time::timeout(
|
||||
Duration::from_secs(30),
|
||||
registration_client.register_lp(...)
|
||||
).await?
|
||||
```
|
||||
|
||||
5. **Secure local key storage**:
|
||||
- Use OS keychain for LP private keys
|
||||
- Don't log or expose keys
|
||||
|
||||
### For Network Operators
|
||||
|
||||
**High Priority:**
|
||||
|
||||
1. **Deploy monitoring infrastructure**:
|
||||
- Prometheus + Grafana for metrics
|
||||
- Alerting on security-relevant metrics
|
||||
- Correlation of events across gateways
|
||||
|
||||
2. **Incident response plan**:
|
||||
- Procedure for gateway compromise
|
||||
- Key rotation workflow
|
||||
- Client notification mechanism
|
||||
|
||||
3. **Regular security audits**:
|
||||
- External audit of Noise implementation
|
||||
- Penetration testing of LP endpoints
|
||||
- Review of credential verification logic
|
||||
|
||||
**Medium Priority:**
|
||||
|
||||
4. **Threat intelligence**:
|
||||
- Monitor for known attacks on Noise protocol
|
||||
- Track quantum computing advances
|
||||
- Plan PQ migration timeline
|
||||
|
||||
## Compliance Considerations
|
||||
|
||||
### Data Protection (GDPR, etc.)
|
||||
|
||||
**Personal Data Collected:**
|
||||
- Client IP address (connection metadata)
|
||||
- Credential nullifiers (pseudonymous identifiers)
|
||||
- Timestamps (connection events)
|
||||
|
||||
**Data Retention:**
|
||||
- IP addresses: Not stored beyond connection duration
|
||||
- Nullifiers: Stored until credential expiry + grace period
|
||||
- Logs: Configurable retention (default: 7 days)
|
||||
|
||||
**Privacy Protections:**
|
||||
- Nullifiers pseudonymous (not linkable to real identity)
|
||||
- No PII collected or stored
|
||||
- Credentials use blind signatures (gateway doesn't learn identity)
|
||||
|
||||
### Security Compliance
|
||||
|
||||
**SOC 2 / ISO 27001 Requirements:**
|
||||
|
||||
1. **Access Control**:
|
||||
- LP keys protected (file permissions)
|
||||
- Database access restricted
|
||||
- Principle of least privilege
|
||||
|
||||
2. **Encryption in Transit**:
|
||||
- Noise protocol provides end-to-end encryption
|
||||
- TLS for metrics endpoint (if exposed)
|
||||
|
||||
3. **Logging and Monitoring**:
|
||||
- Security events logged
|
||||
- Metrics for anomaly detection
|
||||
- Audit trail for credential usage
|
||||
|
||||
4. **Incident Response**:
|
||||
- Key rotation procedure
|
||||
- Backup and recovery
|
||||
- Communication plan
|
||||
|
||||
## Audit Checklist
|
||||
|
||||
Before production deployment:
|
||||
|
||||
- [ ] Noise implementation reviewed by cryptographer
|
||||
- [ ] Replay protection tested with edge cases (overflow, concurrency)
|
||||
- [ ] DoS limits tested (connection flood, credential spam)
|
||||
- [ ] Timing attack resistance verified (replay check, credential verification)
|
||||
- [ ] Key storage secured (file permissions, encryption at rest)
|
||||
- [ ] Monitoring and alerting configured
|
||||
- [ ] Incident response plan documented
|
||||
- [ ] Penetration testing performed
|
||||
- [ ] Code review completed
|
||||
- [ ] Dependencies audited (cargo-audit, cargo-deny)
|
||||
|
||||
## References
|
||||
|
||||
### Security Specifications
|
||||
|
||||
- **Noise Protocol Framework**: https://noiseprotocol.org/
|
||||
- **XKpsk3 Analysis**: https://noiseexplorer.com/patterns/XKpsk3/
|
||||
- **Curve25519**: https://cr.yp.to/ecdh.html
|
||||
- **ChaCha20-Poly1305**: RFC 8439
|
||||
- **Blake3**: https://github.com/BLAKE3-team/BLAKE3-specs
|
||||
|
||||
### Security Audits
|
||||
|
||||
- [ ] Noise implementation audit (pending)
|
||||
- [ ] Cryptographic review (pending)
|
||||
- [ ] Penetration test report (pending)
|
||||
|
||||
### Known Vulnerabilities
|
||||
|
||||
*None currently identified. This section will be updated as issues are discovered.*
|
||||
|
||||
## Responsible Disclosure
|
||||
|
||||
If you discover a security vulnerability in LP:
|
||||
|
||||
1. **DO NOT** publish vulnerability details publicly
|
||||
2. Email security@nymtech.net with:
|
||||
- Description of vulnerability
|
||||
- Steps to reproduce
|
||||
- Potential impact
|
||||
- Suggested mitigation (if any)
|
||||
3. Allow 90 days for patch development before public disclosure
|
||||
4. Coordinate disclosure timeline with Nym team
|
||||
|
||||
**Bug Bounty**: Check https://nymtech.net/security for current bounty program.
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use crate::node::ActiveClientsStore;
|
||||
use nym_credential_verification::upgrade_mode::UpgradeModeDetails;
|
||||
use nym_credential_verification::{ecash::EcashManager, BandwidthFlushingBehaviourConfig};
|
||||
use nym_credential_verification::BandwidthFlushingBehaviourConfig;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_gateway_storage::GatewayStorage;
|
||||
use nym_mixnet_client::forwarder::MixForwardingSender;
|
||||
@@ -23,7 +23,7 @@ pub(crate) struct Config {
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CommonHandlerState {
|
||||
pub(crate) cfg: Config,
|
||||
pub(crate) ecash_verifier: Arc<EcashManager>,
|
||||
pub(crate) ecash_verifier: Arc<dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync>,
|
||||
pub(crate) storage: GatewayStorage,
|
||||
pub(crate) local_identity: Arc<ed25519::KeyPair>,
|
||||
pub(crate) metrics: NymNodeMetrics,
|
||||
|
||||
@@ -5,7 +5,6 @@ use crate::node::internal_service_providers::authenticator::error::Authenticator
|
||||
use futures::channel::oneshot;
|
||||
use ipnetwork::IpNetwork;
|
||||
use nym_client_core::{HardcodedTopologyProvider, TopologyProvider};
|
||||
use nym_credential_verification::ecash::EcashManager;
|
||||
use nym_sdk::{mixnet::Recipient, GatewayTransceiver};
|
||||
use nym_task::ShutdownTracker;
|
||||
use nym_wireguard::WireguardGatewayData;
|
||||
@@ -40,7 +39,7 @@ pub struct Authenticator {
|
||||
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
|
||||
custom_gateway_transceiver: Option<Box<dyn GatewayTransceiver + Send + Sync>>,
|
||||
wireguard_gateway_data: WireguardGatewayData,
|
||||
ecash_verifier: Arc<EcashManager>,
|
||||
ecash_verifier: Arc<dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync>,
|
||||
used_private_network_ips: Vec<IpAddr>,
|
||||
shutdown: ShutdownTracker,
|
||||
on_start: Option<oneshot::Sender<OnStartData>>,
|
||||
@@ -52,7 +51,7 @@ impl Authenticator {
|
||||
upgrade_mode_state: UpgradeModeDetails,
|
||||
wireguard_gateway_data: WireguardGatewayData,
|
||||
used_private_network_ips: Vec<IpAddr>,
|
||||
ecash_verifier: Arc<EcashManager>,
|
||||
ecash_verifier: Arc<dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync>,
|
||||
shutdown: ShutdownTracker,
|
||||
) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -43,14 +43,18 @@ impl LpConnectionHandler {
|
||||
// This is secure and simple - each connection gets its own keypair
|
||||
let gateway_keypair = Keypair::default();
|
||||
|
||||
// Receive client's public key via ClientHello message
|
||||
// Receive client's public key and salt via ClientHello message
|
||||
// The client initiates by sending ClientHello as first packet
|
||||
let client_pubkey = self.receive_client_hello().await?;
|
||||
let (client_pubkey, salt) = self.receive_client_hello().await?;
|
||||
|
||||
// Generate or retrieve PSK for this session
|
||||
// TODO(nym-16): Implement proper PSK management
|
||||
// Temporary solution: use gateway's identity public key as PSK
|
||||
let psk = self.state.local_identity.public_key().to_bytes();
|
||||
// Derive PSK using ECDH + Blake3 KDF (nym-109)
|
||||
// Both client and gateway derive the same PSK from their respective keys
|
||||
let psk = nym_lp::derive_psk(
|
||||
gateway_keypair.private_key(),
|
||||
&client_pubkey,
|
||||
&salt,
|
||||
);
|
||||
tracing::trace!("Derived PSK from LP keys and ClientHello salt");
|
||||
|
||||
// Create LP handshake as responder
|
||||
let handshake = LpGatewayHandshake::new_responder(
|
||||
@@ -91,8 +95,53 @@ impl LpConnectionHandler {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receive client's public key via ClientHello message
|
||||
async fn receive_client_hello(&mut self) -> Result<PublicKey, GatewayError> {
|
||||
/// Validates that a ClientHello timestamp is within the acceptable time window.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `client_timestamp` - Unix timestamp (seconds) from ClientHello salt
|
||||
/// * `tolerance_secs` - Maximum acceptable age in seconds
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` if timestamp is valid (within tolerance window)
|
||||
/// * `Err(GatewayError)` if timestamp is too old or too far in the future
|
||||
///
|
||||
/// # Security
|
||||
/// This prevents replay attacks by rejecting stale ClientHello messages.
|
||||
/// The tolerance window should be:
|
||||
/// - Large enough for clock skew + network latency
|
||||
/// - Small enough to limit replay attack window
|
||||
fn validate_timestamp(client_timestamp: u64, tolerance_secs: u64) -> Result<(), GatewayError> {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("System time before UNIX epoch")
|
||||
.as_secs();
|
||||
|
||||
let age = if now >= client_timestamp {
|
||||
now - client_timestamp
|
||||
} else {
|
||||
// Client timestamp is in the future
|
||||
client_timestamp - now
|
||||
};
|
||||
|
||||
if age > tolerance_secs {
|
||||
let direction = if now >= client_timestamp {
|
||||
"old"
|
||||
} else {
|
||||
"future"
|
||||
};
|
||||
return Err(GatewayError::LpProtocolError(format!(
|
||||
"ClientHello timestamp is too {} (age: {}s, tolerance: {}s)",
|
||||
direction, age, tolerance_secs
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receive client's public key and salt via ClientHello message
|
||||
async fn receive_client_hello(&mut self) -> Result<(PublicKey, [u8; 32]), GatewayError> {
|
||||
// Receive first packet which should be ClientHello
|
||||
let packet = self.receive_lp_packet().await?;
|
||||
|
||||
@@ -106,11 +155,38 @@ impl LpConnectionHandler {
|
||||
));
|
||||
}
|
||||
|
||||
// Extract and validate timestamp (nym-110: replay protection)
|
||||
let timestamp = hello_data.extract_timestamp();
|
||||
Self::validate_timestamp(timestamp, self.state.lp_config.timestamp_tolerance_secs)?;
|
||||
|
||||
tracing::debug!(
|
||||
"ClientHello timestamp validated: {} (age: {}s, tolerance: {}s)",
|
||||
timestamp,
|
||||
{
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("System time before UNIX epoch")
|
||||
.as_secs();
|
||||
if now >= timestamp {
|
||||
now - timestamp
|
||||
} else {
|
||||
timestamp - now
|
||||
}
|
||||
},
|
||||
self.state.lp_config.timestamp_tolerance_secs
|
||||
);
|
||||
|
||||
// Convert bytes to PublicKey
|
||||
PublicKey::from_bytes(&hello_data.client_lp_public_key)
|
||||
let client_pubkey = PublicKey::from_bytes(&hello_data.client_lp_public_key)
|
||||
.map_err(|e| GatewayError::LpProtocolError(
|
||||
format!("Invalid client public key: {}", e)
|
||||
))
|
||||
))?;
|
||||
|
||||
// Extract salt for PSK derivation
|
||||
let salt = hello_data.salt;
|
||||
|
||||
Ok((client_pubkey, salt))
|
||||
}
|
||||
other => {
|
||||
Err(GatewayError::LpProtocolError(
|
||||
@@ -263,4 +339,491 @@ impl LpSessionExt for LpSession {
|
||||
|
||||
Ok(LpPacket::new(header, message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bytes::BytesMut;
|
||||
use nym_lp::keypair::Keypair;
|
||||
use nym_lp::message::{ClientHelloData, LpMessage};
|
||||
use nym_lp::packet::{LpHeader, LpPacket};
|
||||
use nym_lp::codec::{serialize_lp_packet, parse_lp_packet};
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use crate::node::ActiveClientsStore;
|
||||
use crate::node::lp_listener::LpConfig;
|
||||
|
||||
// ==================== Test Helpers ====================
|
||||
|
||||
/// Create a minimal test state for handler tests
|
||||
async fn create_minimal_test_state() -> LpHandlerState {
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
// Create in-memory storage for testing
|
||||
let storage = nym_gateway_storage::GatewayStorage::init(":memory:", 100)
|
||||
.await
|
||||
.expect("Failed to create test storage");
|
||||
|
||||
// Create mock ecash manager for testing
|
||||
let ecash_verifier = nym_credential_verification::ecash::MockEcashManager::new(
|
||||
Box::new(storage.clone())
|
||||
);
|
||||
|
||||
LpHandlerState {
|
||||
lp_config: LpConfig {
|
||||
enabled: true,
|
||||
timestamp_tolerance_secs: 30,
|
||||
..Default::default()
|
||||
},
|
||||
ecash_verifier: Arc::new(ecash_verifier) as Arc<dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync>,
|
||||
storage,
|
||||
local_identity: Arc::new(ed25519::KeyPair::new(&mut OsRng)),
|
||||
metrics: nym_node_metrics::NymNodeMetrics::default(),
|
||||
active_clients_store: ActiveClientsStore::new(),
|
||||
wg_peer_controller: None,
|
||||
wireguard_data: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to write an LP packet to a stream with proper framing
|
||||
async fn write_lp_packet_to_stream<W: AsyncWriteExt + Unpin>(
|
||||
stream: &mut W,
|
||||
packet: &LpPacket,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let mut packet_buf = BytesMut::new();
|
||||
serialize_lp_packet(packet, &mut packet_buf)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
|
||||
|
||||
// Write length prefix
|
||||
let len = packet_buf.len() as u32;
|
||||
stream.write_all(&len.to_be_bytes()).await?;
|
||||
|
||||
// Write packet data
|
||||
stream.write_all(&packet_buf).await?;
|
||||
stream.flush().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper to read an LP packet from a stream with proper framing
|
||||
async fn read_lp_packet_from_stream<R: AsyncReadExt + Unpin>(
|
||||
stream: &mut R,
|
||||
) -> Result<LpPacket, std::io::Error> {
|
||||
// Read length prefix
|
||||
let mut len_buf = [0u8; 4];
|
||||
stream.read_exact(&mut len_buf).await?;
|
||||
let packet_len = u32::from_be_bytes(len_buf) as usize;
|
||||
|
||||
// Read packet data
|
||||
let mut packet_buf = vec![0u8; packet_len];
|
||||
stream.read_exact(&mut packet_buf).await?;
|
||||
|
||||
// Parse packet
|
||||
parse_lp_packet(&packet_buf)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
|
||||
}
|
||||
|
||||
// ==================== Existing Tests ====================
|
||||
|
||||
#[test]
|
||||
fn test_validate_timestamp_current() {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
// Current timestamp should always pass
|
||||
assert!(LpConnectionHandler::validate_timestamp(now, 30).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_timestamp_within_tolerance() {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
// 10 seconds old, tolerance 30s -> should pass
|
||||
let old_timestamp = now - 10;
|
||||
assert!(LpConnectionHandler::validate_timestamp(old_timestamp, 30).is_ok());
|
||||
|
||||
// 10 seconds in future, tolerance 30s -> should pass
|
||||
let future_timestamp = now + 10;
|
||||
assert!(LpConnectionHandler::validate_timestamp(future_timestamp, 30).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_timestamp_too_old() {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
// 60 seconds old, tolerance 30s -> should fail
|
||||
let old_timestamp = now - 60;
|
||||
let result = LpConnectionHandler::validate_timestamp(old_timestamp, 30);
|
||||
assert!(result.is_err());
|
||||
assert!(format!("{:?}", result).contains("too old"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_timestamp_too_far_future() {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
// 60 seconds in future, tolerance 30s -> should fail
|
||||
let future_timestamp = now + 60;
|
||||
let result = LpConnectionHandler::validate_timestamp(future_timestamp, 30);
|
||||
assert!(result.is_err());
|
||||
assert!(format!("{:?}", result).contains("too future"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_timestamp_boundary() {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
// Exactly at tolerance boundary -> should pass
|
||||
let boundary_timestamp = now - 30;
|
||||
assert!(LpConnectionHandler::validate_timestamp(boundary_timestamp, 30).is_ok());
|
||||
|
||||
// Just beyond boundary -> should fail
|
||||
let beyond_timestamp = now - 31;
|
||||
assert!(LpConnectionHandler::validate_timestamp(beyond_timestamp, 30).is_err());
|
||||
}
|
||||
|
||||
// ==================== Packet I/O Tests ====================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_receive_lp_packet_valid() {
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
|
||||
// Bind to localhost
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
|
||||
// Spawn server task
|
||||
let server_task = tokio::spawn(async move {
|
||||
let (stream, remote_addr) = listener.accept().await.unwrap();
|
||||
let state = create_minimal_test_state().await;
|
||||
let mut handler = LpConnectionHandler::new(stream, remote_addr, state);
|
||||
handler.receive_lp_packet().await
|
||||
});
|
||||
|
||||
// Connect as client
|
||||
let mut client_stream = TcpStream::connect(addr).await.unwrap();
|
||||
|
||||
// Send a valid packet from client side
|
||||
let packet = LpPacket::new(
|
||||
LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: 42,
|
||||
counter: 0,
|
||||
},
|
||||
LpMessage::Busy,
|
||||
);
|
||||
write_lp_packet_to_stream(&mut client_stream, &packet).await.unwrap();
|
||||
|
||||
// Handler should receive and parse it correctly
|
||||
let received = server_task.await.unwrap().unwrap();
|
||||
assert_eq!(received.header().protocol_version, 1);
|
||||
assert_eq!(received.header().session_id, 42);
|
||||
assert_eq!(received.header().counter, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_receive_lp_packet_exceeds_max_size() {
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
|
||||
let server_task = tokio::spawn(async move {
|
||||
let (stream, remote_addr) = listener.accept().await.unwrap();
|
||||
let state = create_minimal_test_state().await;
|
||||
let mut handler = LpConnectionHandler::new(stream, remote_addr, state);
|
||||
handler.receive_lp_packet().await
|
||||
});
|
||||
|
||||
let mut client_stream = TcpStream::connect(addr).await.unwrap();
|
||||
|
||||
// Send a packet size that exceeds MAX_PACKET_SIZE (64KB)
|
||||
let oversized_len: u32 = 70000; // > 65536
|
||||
client_stream.write_all(&oversized_len.to_be_bytes()).await.unwrap();
|
||||
client_stream.flush().await.unwrap();
|
||||
|
||||
// Handler should reject it
|
||||
let result = server_task.await.unwrap();
|
||||
assert!(result.is_err());
|
||||
let err_msg = format!("{:?}", result.unwrap_err());
|
||||
assert!(err_msg.contains("exceeds maximum"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_lp_packet_valid() {
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
|
||||
let server_task = tokio::spawn(async move {
|
||||
let (stream, remote_addr) = listener.accept().await.unwrap();
|
||||
let state = create_minimal_test_state().await;
|
||||
let mut handler = LpConnectionHandler::new(stream, remote_addr, state);
|
||||
|
||||
let packet = LpPacket::new(
|
||||
LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: 99,
|
||||
counter: 5,
|
||||
},
|
||||
LpMessage::Busy,
|
||||
);
|
||||
handler.send_lp_packet(&packet).await
|
||||
});
|
||||
|
||||
let mut client_stream = TcpStream::connect(addr).await.unwrap();
|
||||
|
||||
// Wait for server to send
|
||||
server_task.await.unwrap().unwrap();
|
||||
|
||||
// Client should receive it correctly
|
||||
let received = read_lp_packet_from_stream(&mut client_stream).await.unwrap();
|
||||
assert_eq!(received.header().session_id, 99);
|
||||
assert_eq!(received.header().counter, 5);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_receive_handshake_message() {
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
|
||||
let handshake_data = vec![1, 2, 3, 4, 5, 6, 7, 8];
|
||||
let expected_data = handshake_data.clone();
|
||||
|
||||
let server_task = tokio::spawn(async move {
|
||||
let (stream, remote_addr) = listener.accept().await.unwrap();
|
||||
let state = create_minimal_test_state().await;
|
||||
let mut handler = LpConnectionHandler::new(stream, remote_addr, state);
|
||||
|
||||
let packet = LpPacket::new(
|
||||
LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: 100,
|
||||
counter: 10,
|
||||
},
|
||||
LpMessage::Handshake(handshake_data),
|
||||
);
|
||||
handler.send_lp_packet(&packet).await
|
||||
});
|
||||
|
||||
let mut client_stream = TcpStream::connect(addr).await.unwrap();
|
||||
server_task.await.unwrap().unwrap();
|
||||
|
||||
let received = read_lp_packet_from_stream(&mut client_stream).await.unwrap();
|
||||
assert_eq!(received.header().session_id, 100);
|
||||
assert_eq!(received.header().counter, 10);
|
||||
match received.message() {
|
||||
LpMessage::Handshake(data) => assert_eq!(data, &expected_data),
|
||||
_ => panic!("Expected Handshake message"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_receive_encrypted_data_message() {
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
|
||||
let encrypted_payload = vec![42u8; 256];
|
||||
let expected_payload = encrypted_payload.clone();
|
||||
|
||||
let server_task = tokio::spawn(async move {
|
||||
let (stream, remote_addr) = listener.accept().await.unwrap();
|
||||
let state = create_minimal_test_state().await;
|
||||
let mut handler = LpConnectionHandler::new(stream, remote_addr, state);
|
||||
|
||||
let packet = LpPacket::new(
|
||||
LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: 200,
|
||||
counter: 20,
|
||||
},
|
||||
LpMessage::EncryptedData(encrypted_payload),
|
||||
);
|
||||
handler.send_lp_packet(&packet).await
|
||||
});
|
||||
|
||||
let mut client_stream = TcpStream::connect(addr).await.unwrap();
|
||||
server_task.await.unwrap().unwrap();
|
||||
|
||||
let received = read_lp_packet_from_stream(&mut client_stream).await.unwrap();
|
||||
assert_eq!(received.header().session_id, 200);
|
||||
assert_eq!(received.header().counter, 20);
|
||||
match received.message() {
|
||||
LpMessage::EncryptedData(data) => assert_eq!(data, &expected_payload),
|
||||
_ => panic!("Expected EncryptedData message"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_send_receive_client_hello_message() {
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use nym_lp::message::ClientHelloData;
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
|
||||
let client_key = [7u8; 32];
|
||||
let hello_data = ClientHelloData::new_with_fresh_salt(client_key, 1);
|
||||
let expected_salt = hello_data.salt; // Clone salt before moving hello_data
|
||||
|
||||
let server_task = tokio::spawn(async move {
|
||||
let (stream, remote_addr) = listener.accept().await.unwrap();
|
||||
let state = create_minimal_test_state().await;
|
||||
let mut handler = LpConnectionHandler::new(stream, remote_addr, state);
|
||||
|
||||
let packet = LpPacket::new(
|
||||
LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: 300,
|
||||
counter: 30,
|
||||
},
|
||||
LpMessage::ClientHello(hello_data),
|
||||
);
|
||||
handler.send_lp_packet(&packet).await
|
||||
});
|
||||
|
||||
let mut client_stream = TcpStream::connect(addr).await.unwrap();
|
||||
server_task.await.unwrap().unwrap();
|
||||
|
||||
let received = read_lp_packet_from_stream(&mut client_stream).await.unwrap();
|
||||
assert_eq!(received.header().session_id, 300);
|
||||
assert_eq!(received.header().counter, 30);
|
||||
match received.message() {
|
||||
LpMessage::ClientHello(data) => {
|
||||
assert_eq!(data.client_lp_public_key, client_key);
|
||||
assert_eq!(data.protocol_version, 1);
|
||||
assert_eq!(data.salt, expected_salt);
|
||||
}
|
||||
_ => panic!("Expected ClientHello message"),
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== receive_client_hello Tests ====================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_receive_client_hello_valid() {
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
|
||||
let server_task = tokio::spawn(async move {
|
||||
let (stream, remote_addr) = listener.accept().await.unwrap();
|
||||
let state = create_minimal_test_state().await;
|
||||
let mut handler = LpConnectionHandler::new(stream, remote_addr, state);
|
||||
handler.receive_client_hello().await
|
||||
});
|
||||
|
||||
let mut client_stream = TcpStream::connect(addr).await.unwrap();
|
||||
|
||||
// Create and send valid ClientHello
|
||||
let client_keypair = Keypair::default();
|
||||
let hello_data = ClientHelloData::new_with_fresh_salt(
|
||||
client_keypair.public_key().to_bytes(),
|
||||
1, // protocol version
|
||||
);
|
||||
let packet = LpPacket::new(
|
||||
LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: 0,
|
||||
counter: 0,
|
||||
},
|
||||
LpMessage::ClientHello(hello_data.clone()),
|
||||
);
|
||||
write_lp_packet_to_stream(&mut client_stream, &packet).await.unwrap();
|
||||
|
||||
// Handler should receive and parse it
|
||||
let result = server_task.await.unwrap();
|
||||
assert!(result.is_ok());
|
||||
|
||||
let (pubkey, salt) = result.unwrap();
|
||||
assert_eq!(pubkey.as_bytes(), &client_keypair.public_key().to_bytes());
|
||||
assert_eq!(salt, hello_data.salt);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_receive_client_hello_timestamp_too_old() {
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
|
||||
let server_task = tokio::spawn(async move {
|
||||
let (stream, remote_addr) = listener.accept().await.unwrap();
|
||||
let state = create_minimal_test_state().await;
|
||||
let mut handler = LpConnectionHandler::new(stream, remote_addr, state);
|
||||
handler.receive_client_hello().await
|
||||
});
|
||||
|
||||
let mut client_stream = TcpStream::connect(addr).await.unwrap();
|
||||
|
||||
// Create ClientHello with old timestamp
|
||||
let client_keypair = Keypair::default();
|
||||
let mut hello_data = ClientHelloData::new_with_fresh_salt(
|
||||
client_keypair.public_key().to_bytes(),
|
||||
1,
|
||||
);
|
||||
|
||||
// Manually set timestamp to be very old (100 seconds ago)
|
||||
let old_timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() - 100;
|
||||
hello_data.salt[..8].copy_from_slice(&old_timestamp.to_le_bytes());
|
||||
|
||||
let packet = LpPacket::new(
|
||||
LpHeader {
|
||||
protocol_version: 1,
|
||||
session_id: 0,
|
||||
counter: 0,
|
||||
},
|
||||
LpMessage::ClientHello(hello_data),
|
||||
);
|
||||
write_lp_packet_to_stream(&mut client_stream, &packet).await.unwrap();
|
||||
|
||||
// Should fail with timestamp error
|
||||
let result = server_task.await.unwrap();
|
||||
assert!(result.is_err());
|
||||
// Note: Can't use unwrap_err() directly because PublicKey doesn't implement Debug
|
||||
// Just check that it failed
|
||||
match result {
|
||||
Err(e) => {
|
||||
let err_msg = format!("{}", e);
|
||||
assert!(err_msg.contains("too old"), "Expected 'too old' in error, got: {}", err_msg);
|
||||
}
|
||||
Ok(_) => panic!("Expected error but got success"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
use crate::error::GatewayError;
|
||||
use crate::node::ActiveClientsStore;
|
||||
use nym_credential_verification::ecash::EcashManager;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_gateway_storage::GatewayStorage;
|
||||
use nym_node_metrics::NymNodeMetrics;
|
||||
@@ -42,6 +41,17 @@ pub struct LpConfig {
|
||||
/// Maximum concurrent connections
|
||||
#[serde(default = "default_max_connections")]
|
||||
pub max_connections: usize,
|
||||
|
||||
/// Maximum acceptable age of ClientHello timestamp in seconds (default: 30)
|
||||
///
|
||||
/// ClientHello messages with timestamps older than this will be rejected
|
||||
/// to prevent replay attacks. Value should be:
|
||||
/// - Large enough to account for clock skew and network latency
|
||||
/// - Small enough to limit replay attack window
|
||||
///
|
||||
/// Recommended: 30-60 seconds
|
||||
#[serde(default = "default_timestamp_tolerance_secs")]
|
||||
pub timestamp_tolerance_secs: u64,
|
||||
}
|
||||
|
||||
impl Default for LpConfig {
|
||||
@@ -52,6 +62,7 @@ impl Default for LpConfig {
|
||||
control_port: default_control_port(),
|
||||
data_port: default_data_port(),
|
||||
max_connections: default_max_connections(),
|
||||
timestamp_tolerance_secs: default_timestamp_tolerance_secs(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,11 +83,15 @@ fn default_max_connections() -> usize {
|
||||
10000
|
||||
}
|
||||
|
||||
fn default_timestamp_tolerance_secs() -> u64 {
|
||||
30 // 30 seconds - balances security vs clock skew tolerance
|
||||
}
|
||||
|
||||
/// Shared state for LP connection handlers
|
||||
#[derive(Clone)]
|
||||
pub struct LpHandlerState {
|
||||
/// Ecash verifier for bandwidth credentials
|
||||
pub ecash_verifier: Arc<EcashManager>,
|
||||
pub ecash_verifier: Arc<dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync>,
|
||||
|
||||
/// Storage backend for persistence
|
||||
pub storage: GatewayStorage,
|
||||
@@ -95,6 +110,9 @@ pub struct LpHandlerState {
|
||||
|
||||
/// WireGuard gateway data (contains keypair and config)
|
||||
pub wireguard_data: Option<WireguardGatewayData>,
|
||||
|
||||
/// LP configuration (for timestamp validation, etc.)
|
||||
pub lp_config: LpConfig,
|
||||
}
|
||||
|
||||
/// LP listener that accepts TCP connections on port 41264
|
||||
|
||||
@@ -267,7 +267,7 @@ async fn register_wg_peer(
|
||||
async fn store_client_bandwidth(
|
||||
client_id: String,
|
||||
bandwidth: i64,
|
||||
storage: &nym_gateway_storage::GatewayStorage,
|
||||
_storage: &nym_gateway_storage::GatewayStorage,
|
||||
) -> Result<(), GatewayError> {
|
||||
// This would integrate with the actual bandwidth storage
|
||||
// For now, just log it
|
||||
|
||||
@@ -249,13 +249,13 @@ impl GatewayTasksBuilder {
|
||||
Ok(Arc::new(ecash_manager))
|
||||
}
|
||||
|
||||
async fn ecash_manager(&mut self) -> Result<Arc<EcashManager>, GatewayError> {
|
||||
async fn ecash_manager(&mut self) -> Result<Arc<dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync>, GatewayError> {
|
||||
match self.ecash_manager.clone() {
|
||||
Some(cached) => Ok(cached),
|
||||
Some(cached) => Ok(cached as Arc<dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync>),
|
||||
None => {
|
||||
let manager = self.build_ecash_manager().await?;
|
||||
self.ecash_manager = Some(manager.clone());
|
||||
Ok(manager)
|
||||
Ok(manager as Arc<dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync>)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,6 +308,7 @@ impl GatewayTasksBuilder {
|
||||
active_clients_store,
|
||||
wg_peer_controller,
|
||||
wireguard_data: self.wireguard_data.as_ref().map(|wd| wd.inner.clone()),
|
||||
lp_config: self.config.lp.clone(),
|
||||
};
|
||||
|
||||
// Parse bind address from config
|
||||
|
||||
@@ -54,10 +54,6 @@ pub struct LpRegistrationClient {
|
||||
/// Created during handshake initiation (nym-79).
|
||||
state_machine: Option<LpStateMachine>,
|
||||
|
||||
/// Pre-shared key for Noise protocol (PSK).
|
||||
/// Generated randomly per registration for ephemeral LP sessions.
|
||||
psk: [u8; 32],
|
||||
|
||||
/// Client's IP address for registration metadata.
|
||||
client_ip: IpAddr,
|
||||
|
||||
@@ -72,18 +68,17 @@ impl LpRegistrationClient {
|
||||
/// * `local_keypair` - Client's LP keypair for Noise protocol
|
||||
/// * `gateway_public_key` - Gateway's public key
|
||||
/// * `gateway_lp_address` - Gateway's LP listener socket address
|
||||
/// * `psk` - Pre-shared key (use `new_with_default_psk()` for random generation)
|
||||
/// * `client_ip` - Client IP address for registration
|
||||
/// * `config` - Configuration for timeouts and TCP parameters (use `LpConfig::default()`)
|
||||
///
|
||||
/// # Note
|
||||
/// This creates the client but does not establish the connection.
|
||||
/// Call `connect()` to establish the TCP connection.
|
||||
/// PSK is derived automatically during handshake using ECDH + Blake3 KDF (nym-109).
|
||||
pub fn new(
|
||||
local_keypair: Arc<Keypair>,
|
||||
gateway_public_key: PublicKey,
|
||||
gateway_lp_address: SocketAddr,
|
||||
psk: [u8; 32],
|
||||
client_ip: IpAddr,
|
||||
config: LpConfig,
|
||||
) -> Self {
|
||||
@@ -93,13 +88,12 @@ impl LpRegistrationClient {
|
||||
gateway_public_key,
|
||||
gateway_lp_address,
|
||||
state_machine: None,
|
||||
psk,
|
||||
client_ip,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new LP registration client with a randomly generated PSK.
|
||||
/// Creates a new LP registration client with default configuration.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `local_keypair` - Client's LP keypair for Noise protocol
|
||||
@@ -107,26 +101,19 @@ impl LpRegistrationClient {
|
||||
/// * `gateway_lp_address` - Gateway's LP listener socket address
|
||||
/// * `client_ip` - Client IP address for registration
|
||||
///
|
||||
/// Generates a fresh random 32-byte PSK for each registration.
|
||||
/// Since LP is registration-only, PSKs are ephemeral and don't need persistence.
|
||||
/// Uses default config (LpConfig::default()) with sane timeout and TCP parameters.
|
||||
/// For testing with a specific PSK or custom config, use `new()` directly.
|
||||
/// PSK is derived automatically during handshake using ECDH + Blake3 KDF (nym-109).
|
||||
/// For custom config, use `new()` directly.
|
||||
pub fn new_with_default_psk(
|
||||
local_keypair: Arc<Keypair>,
|
||||
gateway_public_key: PublicKey,
|
||||
gateway_lp_address: SocketAddr,
|
||||
client_ip: IpAddr,
|
||||
) -> Self {
|
||||
// Generate random PSK for this registration
|
||||
use rand::Rng;
|
||||
let mut psk = [0u8; 32];
|
||||
rand::thread_rng().fill(&mut psk);
|
||||
|
||||
Self::new(
|
||||
local_keypair,
|
||||
gateway_public_key,
|
||||
gateway_lp_address,
|
||||
psk,
|
||||
client_ip,
|
||||
LpConfig::default(),
|
||||
)
|
||||
@@ -245,12 +232,41 @@ impl LpRegistrationClient {
|
||||
|
||||
tracing::debug!("Starting LP handshake as initiator");
|
||||
|
||||
// Create state machine as initiator
|
||||
// Step 1: Generate ClientHelloData with fresh salt (timestamp + nonce)
|
||||
let client_hello_data = nym_lp::ClientHelloData::new_with_fresh_salt(
|
||||
self.local_keypair.public_key().to_bytes(),
|
||||
1, // protocol_version
|
||||
);
|
||||
let salt = client_hello_data.salt;
|
||||
|
||||
tracing::trace!("Generated ClientHello with timestamp: {}", client_hello_data.extract_timestamp());
|
||||
|
||||
// Step 2: Send ClientHello as first packet (before Noise handshake)
|
||||
let client_hello_header = nym_lp::packet::LpHeader::new(
|
||||
0, // session_id not yet established
|
||||
0, // counter starts at 0
|
||||
);
|
||||
let client_hello_packet = nym_lp::LpPacket::new(
|
||||
client_hello_header,
|
||||
nym_lp::LpMessage::ClientHello(client_hello_data),
|
||||
);
|
||||
Self::send_packet(stream, &client_hello_packet).await?;
|
||||
tracing::debug!("Sent ClientHello packet");
|
||||
|
||||
// Step 3: Derive PSK using ECDH + Blake3 KDF
|
||||
let psk = nym_lp::derive_psk(
|
||||
self.local_keypair.private_key(),
|
||||
&self.gateway_public_key,
|
||||
&salt,
|
||||
);
|
||||
tracing::trace!("Derived PSK from identity keys and salt");
|
||||
|
||||
// Step 4: Create state machine as initiator with derived PSK
|
||||
let mut state_machine = LpStateMachine::new(
|
||||
true, // is_initiator
|
||||
&*self.local_keypair,
|
||||
&self.gateway_public_key,
|
||||
&self.psk,
|
||||
&psk,
|
||||
)?;
|
||||
|
||||
// Start handshake - client (initiator) sends first
|
||||
|
||||
@@ -42,6 +42,7 @@ impl NymNode {
|
||||
host: "127.0.0.1".to_string(),
|
||||
custom_http_port: Some(self.http_port),
|
||||
identity_key: self.identity_key.clone(),
|
||||
lp_address: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user