addressing LP PR comments

This commit is contained in:
Jędrzej Stuczyński
2026-03-03 14:42:48 +00:00
parent 8de574ec97
commit 28c1637198
24 changed files with 84 additions and 161 deletions
Generated
+1 -1
View File
@@ -6875,7 +6875,7 @@ dependencies = [
[[package]]
name = "nym-lp"
version = "0.1.0"
version = "1.20.4"
dependencies = [
"anyhow",
"bs58",
+2
View File
@@ -448,8 +448,10 @@ nym-http-api-common = { version = "1.20.4", path = "common/http-api-common", def
nym-id = { version = "1.20.4", path = "common/nym-id" }
nym-ip-packet-client = { version = "1.20.4", path = "nym-ip-packet-client" }
nym-ip-packet-requests = { version = "1.20.4", path = "common/ip-packet-requests" }
nym-lp = { version = "1.20.4", path = "common/nym-lp" }
nym-kkt = { version = "0.1.0", path = "common/nym-kkt" }
nym-kkt-ciphersuite = { version = "1.20.4", path = "common/nym-kkt-ciphersuite" }
nym-kkt-context = { version = "1.20.4", path = "common/nym-kkt-context" }
nym-metrics = { version = "1.20.4", path = "common/nym-metrics" }
nym-mixnet-client = { version = "1.20.4", path = "common/client-libs/mixnet-client" }
nym-mixnet-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
+2 -4
View File
@@ -511,14 +511,12 @@ mod tests {
#[test]
fn test_key_conversion() {
let dalek_kp = super::KeyPair::new(&mut rand::thread_rng());
let dalek_kp = KeyPair::new(&mut rand::thread_rng());
let mut dalek_private_key_bytes = dalek_kp.private_key().as_bytes().to_owned();
libcrux_curve25519::clamp(&mut dalek_private_key_bytes);
let libcrux_private_key =
libcrux_psq::handshake::types::DHPrivateKey::from_bytes(&dalek_private_key_bytes)
.unwrap();
let libcrux_private_key = DHPrivateKey::from_bytes(&dalek_private_key_bytes).unwrap();
let libcrux_public_key = libcrux_private_key.to_public();
assert_eq!(libcrux_public_key.as_ref(), dalek_kp.public_key.as_bytes());
+2 -2
View File
@@ -12,9 +12,9 @@ num_enum = { workspace = true }
strum = { workspace = true }
# internal
nym-crypto = { path = "../crypto", features = ["hashing"] }
nym-crypto = { workspace = true, features = ["hashing"] }
nym-kkt-ciphersuite = { workspace = true, features = ["digests"] }
nym-kkt-context = { path = "../nym-kkt-context" }
nym-kkt-context = { workspace = true }
nym-pemstore = { workspace = true }
libcrux-kem = { workspace = true }
+3 -2
View File
@@ -10,6 +10,7 @@ use crate::error::KKTError;
pub const MAX_PAYLOAD_LEN: usize = 1_000_000;
const CARRIER_KDF_INFO_TX: &str = "CARRIER_V1_KDF_TX";
const CARRIER_KDF_INFO_RX: &str = "CARRIER_V1_KDF_RX";
const CARRIER_KKT_AAD: &[u8] = b"kkt-carrier-v1";
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct Carrier {
@@ -107,7 +108,7 @@ impl Carrier {
&self.tx_key,
plaintext,
&mut output_buffer,
b"kkt-carrier-v1",
CARRIER_KKT_AAD,
&as_nonce_bytes(self.tx_counter),
)?;
@@ -126,7 +127,7 @@ impl Carrier {
&self.rx_key,
&mut output_buffer,
ciphertext,
b"kkt-carrier-v1",
CARRIER_KKT_AAD,
&as_nonce_bytes(self.rx_counter),
)?;
+4 -4
View File
@@ -40,16 +40,16 @@ pub enum KKTError {
#[error("Local Function Input Error: {}", info)]
FunctionInputError { info: &'static str },
#[error("{}", info)]
#[error("{info}")]
X25519Error { info: &'static str },
#[error("{}", info)]
#[error("{info}")]
AEADError { info: &'static str },
#[error("{}", info)]
#[error("{info}")]
DecodingError { info: &'static str },
#[error("{}", info)]
#[error("{info}")]
UnsupportedAlgorithm { info: &'static str },
#[error("Generic libcrux error")]
+2 -2
View File
@@ -114,14 +114,14 @@ impl KKTRequestPlaintext {
}
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN);
let mut out = Vec::with_capacity(Self::SIZE);
out.extend_from_slice(self.dh_pubkey.as_ref());
out.extend_from_slice(self.masked_version_bytes.as_slice());
out
}
pub(crate) fn try_from_bytes(b: &[u8]) -> Result<Self, KKTError> {
if b.len() != x25519::PUBLIC_KEY_LENGTH + MASKED_BYTE_LEN {
if b.len() != Self::SIZE {
return Err(KKTError::FrameDecodingError {
info: "the KKTRequest frame has invalid length".to_string(),
});
+3 -5
View File
@@ -4,15 +4,13 @@
//! Post-Quantum Re-Key Protocol
/// This module implements a stateless post-quantum re-keying protocol in one round-trip.
/// We currently support MlKem768 and XWing.
/// We currently support MlKem768.
///
/// This protocol is safe if it runs under a trusted secure channel.
///
/// Bandwidth costs:
/// Request (MlKem768): 1216 bytes
/// Response (MlKem768): 1088 bytes
/// Request (XWing): 1248 bytes
/// Response (XWing): 1120 bytes
use libcrux_kem::*;
use nym_crypto::hkdf::blake3::derive_key_blake3;
use nym_kkt_ciphersuite::{KEM, mceliece, ml_kem768, x25519, xwing};
@@ -60,7 +58,7 @@ impl RekeyInitiator {
///
/// Inputs:
/// rng: something that implements CryptoRng + RngCore
/// kem: a KEM algorithm (we currently support MlKem768 and XWing)
/// kem: a KEM algorithm (we currently support MlKem768 only)
///
/// Outputs:
/// RekeyInitiator: A struct which contains the decapsulation key, the salt and the kem algorithm in use.
@@ -171,7 +169,7 @@ where
Some(num) => match num {
// If message length is 1216 (32 + 1184) then the algorithm should be MlKem768
ml_kem768::PUBLIC_KEY_LENGTH => Algorithm::MlKem768,
// If message length is 1248 (32 + 1216) then the algorithm should be MlKem768
// If message length is 1248 (32 + 1216) then the algorithm should be xwing
xwing::PUBLIC_KEY_LENGTH => Algorithm::XWingKemDraft06,
// We don't support McEliece because the keys are massive.
// If this is a deal-breaker, users can start a new session with PSQ which can use McEliece.
+1 -1
View File
@@ -146,7 +146,7 @@ impl<'a> KKTResponder<'a> {
};
// for now the response payload is empty
let response_payload = Vec::with_capacity(0);
let response_payload = Vec::new();
let frame = KKTFrame::new(local_context, kem_key, response_payload);
+13 -7
View File
@@ -1,8 +1,14 @@
[package]
name = "nym-lp"
version = "0.1.0"
edition = { workspace = true }
license = { workspace = true }
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
readme.workspace = true
version.workspace = true
publish = false
[dependencies]
@@ -11,11 +17,11 @@ bs58 = { workspace = true }
bytes = { workspace = true }
tracing = { workspace = true }
rand09 = { workspace = true }
tls_codec = { workspace = true }
tls_codec = { workspace = true }
tokio = { workspace = true, features = ["net", "io-util"] }
nym-crypto = { path = "../crypto", features = ["hashing"] }
nym-kkt = { path = "../nym-kkt" }
nym-crypto = { workspace = true, features = ["hashing"] }
nym-kkt = { workspace = true }
nym-kkt-ciphersuite = { workspace = true }
# libcrux dependencies for PSQ (Post-Quantum PSK derivation)
@@ -28,7 +34,7 @@ zeroize = { workspace = true, features = ["zeroize_derive"] }
nym-test-utils = { workspace = true, optional = true }
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
criterion = { workspace = true, features = ["html_reports"] }
nym-test-utils = { workspace = true }
anyhow = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
-79
View File
@@ -1,79 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Configuration for LP protocol.
//!
//! LP security stack = KKT (key fetch) → PSQ (PQ PSK) → Noise (transport).
//! KEM algorithm selection affects only PSQ layer. Noise always uses X25519 DH.
//! Migration to PQ KEMs (MlKem768, XWing) requires only config change.
use nym_kkt::ciphersuite::KEM;
use serde::{Deserialize, Serialize};
use std::time::Duration;
/// Default PSK time-to-live (1 hour, matches psk.rs implementation).
pub const DEFAULT_PSK_TTL_SECS: u64 = 3600;
/// Configuration for LP protocol.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LpConfig {
/// KEM algorithm for PSQ key encapsulation.
/// Supported KEMs: MlKem768, McEliece
#[serde(with = "kem_serde")]
pub kem_algorithm: KEM,
/// PSK time-to-live in seconds.
pub psk_ttl_secs: u64,
/// Enable KKT for authenticated key distribution.
pub enable_kkt: bool,
}
impl Default for LpConfig {
fn default() -> Self {
Self {
kem_algorithm: KEM::MlKem768,
psk_ttl_secs: DEFAULT_PSK_TTL_SECS,
enable_kkt: true,
}
}
}
impl LpConfig {
/// Returns PSK TTL as Duration.
pub fn psk_ttl(&self) -> Duration {
Duration::from_secs(self.psk_ttl_secs)
}
}
mod kem_serde {
use nym_kkt::ciphersuite::KEM;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S>(kem: &KEM, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match kem {
KEM::MlKem768 => "MlKem768",
KEM::McEliece => "McEliece",
KEM::X25519 => return Err(serde::ser::Error::custom("Unsupported KEM: X25519")),
KEM::XWing => return Err(serde::ser::Error::custom("Unsupported KEM: XWing")),
}
.serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<KEM, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
"MlKem768" => Ok(KEM::MlKem768),
"McEliece" => Ok(KEM::McEliece),
"X25519" => Err(serde::de::Error::custom("Unsupported KEM: X25519")),
"XWing" => Err(serde::de::Error::custom("Unsupported KEM: XWing")),
_ => Err(serde::de::Error::custom(format!("Unknown KEM: {}", s))),
}
}
}
+4 -1
View File
@@ -82,7 +82,10 @@ impl Debug for LpLocalPeer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LpLocalPeer")
.field("ciphersuite", &self.ciphersuite)
.field("x25519", &self.x25519.pk)
.field(
"x25519",
&bs58::encode(self.x25519.pk.as_ref()).into_string(),
)
.field("kem_keypairs", &self.kem_keypairs)
.finish()
}
+22 -23
View File
@@ -29,7 +29,7 @@ pub struct LpPeerConfig {
// Determine the hop id.
// Should be 0 if node_initiator is true
// Should be > 1 if is_exit is true
// Should be > 1 && < 16 if is_exit is true
hop_id: u8,
// Determine if the recipient should be an exit node
@@ -198,37 +198,36 @@ impl LpPeerConfig {
}
pub fn serialize(&self) -> [u8; LP_PEER_CONFIG_SIZE] {
let mut output_bytes: [u8; LP_PEER_CONFIG_SIZE] = [0u8; LP_PEER_CONFIG_SIZE];
output_bytes[0..4].copy_from_slice(self.pack_config().as_slice());
let mut output_bytes = [0u8; LP_PEER_CONFIG_SIZE];
output_bytes[0..4].copy_from_slice(&self.pack_config());
output_bytes[4..].copy_from_slice(&self.seed);
output_bytes
}
pub fn deserialize(bytes: &[u8]) -> Result<Self, LpError> {
if bytes.len() != LP_PEER_CONFIG_SIZE {
Err(LpError::DeserializationError(format!(
return Err(LpError::DeserializationError(format!(
"Invalid Lp Config Length ({}), expected ({})",
bytes.len(),
LP_PEER_CONFIG_SIZE
)))
} else {
let (hop_id, is_exit, node_initiator, censorship_resistance) =
Self::unpack_first_byte(bytes[0]);
let mut filler: [u8; FILLER_LEN] = [0u8; FILLER_LEN];
filler.copy_from_slice(&bytes[CONFIG_LEN..CONFIG_LEN + FILLER_LEN]);
let mut seed: [u8; SEED_LEN] = [0u8; SEED_LEN];
seed.copy_from_slice(&bytes[CONFIG_LEN + FILLER_LEN..LP_PEER_CONFIG_SIZE]);
Self::build_checked(
hop_id,
is_exit,
node_initiator,
censorship_resistance,
seed,
filler,
)
)));
}
let (hop_id, is_exit, node_initiator, censorship_resistance) =
Self::unpack_first_byte(bytes[0]);
let mut filler = [0u8; FILLER_LEN];
filler.copy_from_slice(&bytes[CONFIG_LEN..CONFIG_LEN + FILLER_LEN]);
let mut seed = [0u8; SEED_LEN];
seed.copy_from_slice(&bytes[CONFIG_LEN + FILLER_LEN..LP_PEER_CONFIG_SIZE]);
Self::build_checked(
hop_id,
is_exit,
node_initiator,
censorship_resistance,
seed,
filler,
)
}
fn pack_config(&self) -> [u8; 4] {
+1 -1
View File
@@ -44,7 +44,7 @@ pub enum LpAction {
pub type SessionId = [u8; 32];
/// A session in the Lewes Protocol, handling connection state with Noise.
/// A session in the Lewes Protocol..
///
/// Sessions manage connection state, including LP replay protection.
/// Each session has a unique receiving index and sending index for connection identification.
+1 -6
View File
@@ -17,17 +17,12 @@ use crate::session::{LpAction, LpInput};
/// Manages the lifecycle of Lewes Protocol sessions.
///
/// The SessionManager is responsible for creating, storing, and retrieving sessions
#[derive(Default)]
pub struct SessionManager {
/// Manages state machines directly, keyed by lp_id
sessions: HashMap<LpReceiverIndex, LpTransportSession>,
}
impl Default for SessionManager {
fn default() -> Self {
Self::new()
}
}
impl SessionManager {
/// Creates a new session manager with empty session storage.
pub fn new() -> Self {
+2 -1
View File
@@ -79,10 +79,11 @@ async fn read_n_bytes_async_read<R>(reader: &mut R, n: usize) -> Result<Vec<u8>,
where
R: AsyncRead + Unpin,
{
let mut buf = vec![0u8; n];
if n > MAX_HANDSHAKE_PACKET_SIZE {
return Err(LpTransportError::PacketTooBig { size: n });
}
let mut buf = vec![0u8; n];
reader
.read_exact(&mut buf)
.await
+2 -2
View File
@@ -105,9 +105,9 @@ pub struct GatewayTasksBuilder {
shutdown_tracker: ShutdownTracker,
// populated and cached as necessary
use_mock_ecash: bool,
// populated and cached as necessary
ecash_manager:
Option<Arc<dyn nym_credential_verification::ecash::traits::EcashManager + Send + Sync>>,
@@ -226,7 +226,7 @@ impl GatewayTasksBuilder {
> {
// Check if we should use mock ecash for testing
if self.use_mock_ecash {
warn!("Using MockEcashManager for LP testing (credentials NOT verified)");
warn!("Using MockEcashManager for testing (credentials NOT verified)");
let mock_manager = MockEcashManager::new(Box::new(self.storage.clone()));
return Ok(Arc::new(mock_manager)
as Arc<
+3 -8
View File
@@ -172,13 +172,11 @@ pub async fn lp_registration_probe(
let mut lp_outcome = LpProbeResults::default();
// Generate Ed25519 keypair for this connection (X25519 will be derived internally by LP)
// Generate X25519 keypair for this connection
let mut rng09 = rand09::rngs::StdRng::from_os_rng();
let client_x25519_keypair = Arc::new(DHKeyPair::new(&mut rng09));
// Step 0: Derive X25519 keys from Ed25519 for the gateways
// Create LP registration client (uses Ed25519 keys directly, derives X25519 internally)
// Create LP registration client
let mut client = LpRegistrationClient::<TcpStream>::new_with_default_config(
client_x25519_keypair,
peer,
@@ -212,16 +210,13 @@ pub async fn lp_registration_probe(
let mut rng = rand::thread_rng();
let wg_keypair = nym_crypto::asymmetric::x25519::KeyPair::new(&mut rng);
// Convert gateway identity to ed25519 public key
let gateway_ed25519_pubkey = gateway_identity;
// Register using the new packet-per-connection API (returns GatewayData directly)
let ticket_type = TicketType::V1WireguardEntry;
let gateway_data = match client
.register_dvpn(
&mut rng09,
&wg_keypair,
&gateway_ed25519_pubkey,
&gateway_identity,
bandwidth_controller,
ticket_type,
)
+1 -3
View File
@@ -13,8 +13,6 @@ license = "GPL-3.0"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
path = "src/lib.rs"
[dependencies]
async-trait = { workspace = true }
@@ -117,7 +115,7 @@ nym-network-requester = { path = "../service-providers/network-requester" }
nym-ip-packet-router = { path = "../service-providers/ip-packet-router" }
# LP dependencies
nym-lp = { path = "../common/nym-lp" }
nym-lp = { workspace = true }
nym-registration-common = { path = "../common/registration" }
bincode = { workspace = true }
+1 -3
View File
@@ -12,7 +12,7 @@ use nym_lp::session::{LpAction, LpInput};
use nym_lp::transport::LpHandshakeChannel;
use nym_lp::transport::traits::LpTransportChannel;
use nym_lp::{LpTransportSession, packet::message::ExpectedResponseSize};
use nym_metrics::{add_histogram_obs, inc};
use nym_metrics::{add_histogram_obs, inc, inc_by};
use nym_registration_common::{LpRegistrationRequest, RegistrationStatus};
use std::net::SocketAddr;
use std::time::Duration;
@@ -625,8 +625,6 @@ where
/// Emit connection lifecycle metrics
fn emit_lifecycle_metrics(&self, graceful: bool) {
use nym_metrics::inc_by;
// Track connection duration
let duration = self.stats.start_time.elapsed().as_secs_f64();
add_histogram_obs!(
+3 -3
View File
@@ -453,16 +453,16 @@ impl NymNode {
&config.storage_paths.keys.x25519_noise_storage_paths(),
)?;
trace!("attempting to x25519 lp keypair");
trace!("attempting to store x25519 lp keypair");
store_x25519_lp_keypair(
&x25519_lp_keys,
&config.storage_paths.keys.x25519_lp_key_paths(),
)?;
trace!("attempting to mlkem768 keypair");
trace!("attempting to store mlkem768 keypair");
store_mlkem768_keypair(&mlkem, &config.storage_paths.keys.mlkem768_key_paths())?;
trace!("attempting to mceliece keypair");
trace!("attempting to store mceliece keypair");
store_mceliece_keypair(&mceliece, &config.storage_paths.keys.mceliece_key_paths())?;
trace!("creating description file");
+3 -2
View File
@@ -64,7 +64,6 @@ impl LpBasedRegistrationClient {
tracing::debug!("Exit gateway LP address: {exit_address}");
// Generate fresh x25519 keypairs for LP registration
// TODO: persist them for the duration of the sessions
let entry_lp_keypair = Arc::new(DHKeyPair::new(&mut rand09::rng()));
let exit_lp_keypair = Arc::new(DHKeyPair::new(&mut rand09::rng()));
@@ -100,7 +99,7 @@ impl LpBasedRegistrationClient {
tracing::info!("Registering with exit gateway via entry forwarding");
let mut nested_session = NestedLpSession::new(
exit_address,
exit_lp_keypair,
exit_lp_keypair.clone(),
exit_peer,
exit_ciphersuite,
exit_lp_protocol,
@@ -153,6 +152,8 @@ impl LpBasedRegistrationClient {
Ok(RegistrationResult::Lp(Box::new(LpRegistrationResult {
entry_gateway_data,
exit_gateway_data,
entry_lp_keypair,
exit_lp_keypair,
bw_controller: self.bandwidth_controller,
})))
}
@@ -391,7 +391,6 @@ where
let protocol_version = self.gateway_supported_lp_protocol_version;
let connection = self.stream_mut()?;
// TODO:
let session = LpTransportSession::psq_handshake_initiator(
connection,
local_peer,
+8
View File
@@ -3,8 +3,10 @@
use nym_authenticator_client::{AuthClientMixnetListenerHandle, AuthenticatorClient};
use nym_bandwidth_controller::BandwidthTicketProvider;
use nym_lp::peer::DHKeyPair;
use nym_registration_common::{AssignedAddresses, WireguardConfiguration};
use nym_sdk::mixnet::{EventReceiver, MixnetClient};
use std::sync::Arc;
pub enum RegistrationResult {
Mixnet(Box<MixnetRegistrationResult>),
@@ -44,6 +46,12 @@ pub struct LpRegistrationResult {
/// Gateway configuration data from exit gateway
pub exit_gateway_data: WireguardConfiguration,
/// x25519 keypair used on the entry channel
pub entry_lp_keypair: Arc<DHKeyPair>,
/// x25519 keypair used on the exit channel
pub exit_lp_keypair: Arc<DHKeyPair>,
/// Bandwidth controller for credential management
pub bw_controller: Box<dyn BandwidthTicketProvider>,
}