LpSession cleanup

This commit is contained in:
Jędrzej Stuczyński
2026-02-18 16:37:37 +00:00
parent 39ee22e501
commit 63582dd4e1
22 changed files with 2367 additions and 6210 deletions
+7 -7
View File
@@ -397,13 +397,13 @@ prometheus = { version = "0.14.0" }
# libcrux
libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux" }
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" }
libcrux-psq = { git = "https://github.com/cryspen/libcrux" }
libcrux-ml-kem = { git = "https://github.com/cryspen/libcrux" }
libcrux-sha3 = { git = "https://github.com/cryspen/libcrux" }
libcrux-traits = { git = "https://github.com/cryspen/libcrux" }
libcrux-kem = { git = "https://github.com/jstuczyn/libcrux", branch = "jstuczyn/exposed-transport" }
libcrux-ecdh = { git = "https://github.com/jstuczyn/libcrux", branch = "jstuczyn/exposed-transport" }
libcrux-chacha20poly1305 = { git = "https://github.com/jstuczyn/libcrux", branch = "jstuczyn/exposed-transport" }
libcrux-psq = { git = "https://github.com/jstuczyn/libcrux", branch = "jstuczyn/exposed-transport" }
libcrux-ml-kem = { git = "https://github.com/jstuczyn/libcrux", branch = "jstuczyn/exposed-transport" }
libcrux-sha3 = { git = "https://github.com/jstuczyn/libcrux", branch = "jstuczyn/exposed-transport" }
libcrux-traits = { git = "https://github.com/jstuczyn/libcrux", branch = "jstuczyn/exposed-transport" }
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests
nym-api-requests = { version = "1.20.4", path = "nym-api/nym-api-requests" }
+47 -29
View File
@@ -7,11 +7,15 @@ use libcrux_psq::classic_mceliece;
use libcrux_psq::handshake::types::PQEncapsulationKey;
use nym_kkt_ciphersuite::{KEM, mceliece};
use std::fmt::{Debug, Formatter};
use std::sync::Arc;
/// Wrapper around keys used for the KEM exchange
/// with cheap clones thanks to Arc wrappers
pub struct KEMKeys {
mc_eliece: classic_mceliece::KeyPair,
ml_kem768: MlKem768KeyPair,
mc_eliece_pk: Arc<classic_mceliece::PublicKey>,
mc_eliece_sk: Arc<classic_mceliece::SecretKey>,
ml_kem768_pk: Arc<MlKem768PublicKey>,
ml_kem768_sk: Arc<MlKem768PrivateKey>,
}
impl Debug for KEMKeys {
@@ -25,40 +29,61 @@ impl Debug for KEMKeys {
impl KEMKeys {
pub fn new(mc_eliece: classic_mceliece::KeyPair, ml_kem768: MlKem768KeyPair) -> Self {
let (ml_kem768_sk, ml_kem768_pk) = ml_kem768.into_parts();
Self {
mc_eliece,
ml_kem768,
mc_eliece_pk: Arc::new(mc_eliece.pk),
mc_eliece_sk: Arc::new(mc_eliece.sk),
ml_kem768_pk: Arc::new(ml_kem768_pk),
ml_kem768_sk: Arc::new(ml_kem768_sk),
}
}
pub fn encoded_encapsulation_key(&self, kem: KEM) -> Option<&[u8]> {
match kem {
KEM::McEliece => Some(self.mc_eliece.pk.as_ref()),
KEM::MlKem768 => Some(self.ml_kem768.pk()),
KEM::McEliece => Some(self.mc_eliece_pk.as_ref().as_ref()),
KEM::MlKem768 => Some(self.ml_kem768_pk.as_slice()),
// _ => None,
}
}
pub fn encapsulation_key(&self, kem: KEM) -> Option<EncapsulationKey> {
match kem {
KEM::McEliece => Some(EncapsulationKey::McEliece(self.mc_eliece_pk.clone())),
KEM::MlKem768 => Some(EncapsulationKey::MlKem768(self.ml_kem768_pk.clone())),
// _ => None,
}
}
pub fn mc_eliece_encapsulation_key(&self) -> &classic_mceliece::PublicKey {
&self.mc_eliece.pk
&self.mc_eliece_pk
}
pub fn ml_kem768_encapsulation_key(&self) -> &MlKem768PublicKey {
self.ml_kem768.public_key()
self.ml_kem768_pk.as_ref()
}
pub fn mc_eliece_decapsulation_key(&self) -> &classic_mceliece::SecretKey {
&self.mc_eliece.sk
&self.mc_eliece_sk
}
pub fn ml_kem768_decapsulation_key(&self) -> &MlKem768PrivateKey {
self.ml_kem768.private_key()
&self.ml_kem768_sk
}
}
#[derive(Clone)]
pub enum EncapsulationKey {
McEliece(classic_mceliece::PublicKey),
MlKem768(MlKem768PublicKey),
McEliece(Arc<classic_mceliece::PublicKey>),
MlKem768(Arc<MlKem768PublicKey>),
}
impl Debug for EncapsulationKey {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
EncapsulationKey::McEliece(_) => write!(f, "EncapsulationKey::McEliece"),
EncapsulationKey::MlKem768(_) => write!(f, "EncapsulationKey::MlKem768"),
}
}
}
impl EncapsulationKey {
@@ -78,9 +103,11 @@ impl EncapsulationKey {
pub fn try_from_bytes(bytes: Vec<u8>, kem: KEM) -> Result<EncapsulationKey, KKTError> {
match kem {
KEM::MlKem768 => Ok(EncapsulationKey::MlKem768(
MlKem768PublicKey::try_from(bytes.as_slice()).unwrap(),
)),
KEM::MlKem768 => Ok(EncapsulationKey::MlKem768(Arc::new(
MlKem768PublicKey::try_from(bytes.as_slice()).map_err(|_| KKTError::KEMError {
info: "mlkem768 key of invalid length",
})?,
))),
KEM::McEliece => {
let boxed_array: Box<[u8; mceliece::PUBLIC_KEY_LENGTH]> = bytes
.into_boxed_slice()
@@ -89,26 +116,17 @@ impl EncapsulationKey {
info: "mceliece key of invalid length",
})?;
Ok(EncapsulationKey::McEliece(
classic_mceliece::PublicKey::try_from(boxed_array).unwrap(),
))
Ok(EncapsulationKey::McEliece(Arc::new(
classic_mceliece::PublicKey::from(boxed_array),
)))
}
}
}
pub fn as_bytes(&self) -> &[u8] {
match self {
EncapsulationKey::McEliece(k) => k.as_ref(),
EncapsulationKey::MlKem768(k) => k.as_ref(),
}
}
}
impl Debug for EncapsulationKey {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
EncapsulationKey::McEliece(_) => write!(f, "McEliece"),
EncapsulationKey::MlKem768(_) => write!(f, "MlKem768"),
EncapsulationKey::McEliece(k) => k.as_ref().as_ref(),
EncapsulationKey::MlKem768(k) => k.as_ref().as_ref(),
}
}
}
+19
View File
@@ -8,6 +8,25 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::net::TcpStream;
use tracing::debug;
// only used in internal code (and tests)
#[allow(async_fn_in_trait)]
pub trait LpChannel: Sized {
/// Sends a serialised acket over the data stream.
///
/// # Arguments
/// * `packet_data` - The serialised packet to send
///
/// # Errors
/// Returns an error on network transmission fails.
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()>;
/// Receives a data chunk of the set length from the data stream.
///
/// # Errors
/// Returns an error on network transmission fails.
async fn receive_raw_packet(&mut self, len: usize) -> std::io::Result<Vec<u8>>;
}
// only used in internal code (and tests)
#[allow(async_fn_in_trait)]
pub trait LpTransport: Sized {
-11
View File
@@ -7,17 +7,11 @@ publish = false
[dependencies]
thiserror = { workspace = true }
parking_lot = { workspace = true }
snow = { workspace = true }
bs58 = { workspace = true }
serde = { workspace = true }
bytes = { workspace = true }
dashmap = { workspace = true }
sha2 = { workspace = true }
tracing = { workspace = true }
rand = { workspace = true }
rand09 = { workspace = true }
rand_core09 = { package = "rand_core", version = "=0.9.2" }
nym-crypto = { path = "../crypto", features = ["hashing", "asymmetric"] }
nym-kkt = { path = "../nym-kkt" }
@@ -27,10 +21,7 @@ nym-lp-transport = { path = "../nym-lp-transport" }
# libcrux dependencies for PSQ (Post-Quantum PSK derivation)
libcrux-psq = { workspace = true, features = ["test-utils"] }
libcrux-kem = { workspace = true }
tls_codec = { workspace = true }
num_enum = { workspace = true }
chacha20poly1305 = { workspace = true }
zeroize = { workspace = true, features = ["zeroize_derive"] }
# needed for the 'mock 'feature
@@ -38,8 +29,6 @@ nym-test-utils = { workspace = true, optional = true }
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
#rand_chacha = "0.3"
mock_instant = { workspace = true }
nym-crypto = { path = "../crypto", features = ["rand"] }
nym-test-utils = { workspace = true }
anyhow = { workspace = true }
+925 -1279
View File
File diff suppressed because it is too large Load Diff
+11 -25
View File
@@ -2,7 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
use crate::message::MessageType;
use crate::{noise_protocol::NoiseError, replay::ReplayError};
use crate::replay::ReplayError;
use crate::session::SessionId;
use libcrux_psq::handshake::HandshakeError;
use libcrux_psq::handshake::builders::BuilderError;
use libcrux_psq::session::SessionError;
@@ -16,27 +17,9 @@ pub enum LpError {
#[error("IO Error: {0}")]
IoError(#[from] std::io::Error),
// noiserm
#[error("Snow Error: {0}")]
SnowKeyError(#[from] snow::Error),
// noiserm
#[error("Snow Pattern Error: {0}")]
SnowPatternError(String),
// noiserm
#[error("Noise Protocol Error: {0}")]
NoiseError(#[from] NoiseError),
#[error("PSQ Error: {0}")]
PSQError(String),
#[error("Replay detected: {0}")]
Replay(#[from] ReplayError),
#[error("Invalid packet format: {0}")]
InvalidPacketFormat(String),
#[error("Invalid message type: {0}")]
InvalidMessageType(u32),
@@ -81,17 +64,13 @@ pub enum LpError {
LpSessionProcessing,
/// State machine not found.
#[error("State machine not found for lp_id: {lp_id}")]
StateMachineNotFound { lp_id: u32 },
#[error("State machine not found for lp_id: {lp_id:?}")]
StateMachineNotFound { lp_id: SessionId },
/// Ed25519 to X25519 conversion error.
#[error("Ed25519 key conversion error: {0}")]
Ed25519RecoveryError(#[from] Ed25519RecoveryError),
/// Outer AEAD authentication tag verification failed.
#[error("AEAD authentication tag verification failed")]
AeadTagMismatch,
/// Received an LP packet with an incompatible, future, version
#[error("incompatible LP packet version. got: {got}, highest supported: {highest_supported}")]
IncompatibleFuturePacketVersion { got: u8, highest_supported: u8 },
@@ -135,6 +114,9 @@ pub enum LpError {
#[error("failed to run the PSQ session: {inner:?}")]
PSQSessionFailure { inner: SessionError },
#[error("failed to derive a transport channel: {inner:?}")]
TransportDerivationFailure { inner: SessionError },
#[error("the initiator authenticator is not available after ingesting PSQ msg1")]
MissingInitiatorAuthenticator,
}
@@ -149,6 +131,10 @@ impl LpError {
"received unexpected response, got: {got:?}, expected: {expected:?}"
))
}
pub fn invalid_message_type(message_type: u32) -> Self {
LpError::InvalidMessageType(message_type)
}
}
impl From<HandshakeError> for LpError {
-492
View File
@@ -1,492 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! KKT (Key Encapsulation Transport) orchestration for nym-lp sessions.
//!
//! This module provides functions to perform KKT key exchange before establishing
//! an nym-lp session. The KKT protocol allows secure distribution of post-quantum
//! KEM public keys, which are then used with PSQ to derive a strong pre-shared key
//! for the Noise protocol.
//!
//! # Protocol Flow
//!
//! 1. **Client (Initiator)**:
//! - Calls `create_request()` to generate a KKT request
//! - Sends `LpMessage::KKTRequest` to gateway
//! - Receives `LpMessage::KKTResponse` from gateway
//! - Calls `process_response()` to validate and extract gateway's KEM key
//!
//! 2. **Gateway (Responder)**:
//! - Receives `LpMessage::KKTRequest` from client
//! - Calls `handle_request()` to validate request and generate response
//! - Sends `LpMessage::KKTResponse` to client
//!
//! # Example
//!
//! ```ignore
//! use nym_lp::kkt_orchestrator::{create_request, process_response, handle_request};
//! use nym_lp::message::{KKTRequestData, KKTResponseData};
//! use nym_kkt::ciphersuite::{Ciphersuite, KEM, HashFunction, SignatureScheme, EncapsulationKey};
//!
//! // Setup ciphersuite
//! let ciphersuite = Ciphersuite::resolve_ciphersuite(
//! KEM::X25519,
//! HashFunction::Blake3,
//! SignatureScheme::Ed25519,
//! None,
//! ).unwrap();
//!
//! // Client: Create request
//! let (session_secret, client_context, request_data) = create_request(
//! ciphersuite,
//! &client_signing_key,
//! &responder_dh_public_key
//! ).unwrap();
//!
//! // Gateway: Handle request
//! let response_data = handle_request(
//! &request_data,
//! Some(&client_verification_key),
//! &gateway_signing_key,
//! &gateway_dh_private_key,
//! &gateway_kem_public_key,
//! ).unwrap();
//!
//! // Client: Process response
//! let gateway_kem_key = process_response(
//! client_context,
//! &session_secret,
//! &gateway_verification_key,
//! &expected_key_hash,
//! &response_data,
//! ).unwrap();
//! ```
use crate::LpError;
use crate::message::{KKTRequestData, KKTResponseData};
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_kkt::ciphersuite::{Ciphersuite, EncapsulationKey};
use nym_kkt::context::KKTContext;
use nym_kkt::encryption::KKTSessionSecret;
use nym_kkt::kkt::{handle_kem_request, request_kem_key, validate_kem_response};
/// Creates a KKT request to obtain the responder's KEM public key.
///
/// This is called by the **client (initiator)** to begin the KKT exchange.
/// The returned context must be used when processing the response.
///
/// # Arguments
/// * `ciphersuite` - Negotiated ciphersuite (KEM, hash, signature algorithms)
/// * `signing_key` - Client's Ed25519 signing key for authentication
/// * `responder_dh_public_key` - Gateway's x25519 public key (from directory)
///
/// # Returns
/// * `KKTSessionSecret` - Session secret key to encrypt/decrypt KKT messages for this session
/// * `KKTContext` - Context to use when validating the response
/// * `KKTRequestData` - Serialized KKT request frame to send to gateway
///
/// # Errors
/// Returns `LpError::KKTError` if KKT request generation fails.
pub fn create_request(
ciphersuite: Ciphersuite,
signing_key: &ed25519::PrivateKey,
responder_dh_public_key: &x25519::PublicKey,
) -> Result<(KKTSessionSecret, KKTContext, KKTRequestData), LpError> {
// Note: Uses rand 0.9's thread_rng() to match nym-kkt's rand version
let mut rng = rand09::rng();
let (session_secret, context, request_bytes) =
request_kem_key(&mut rng, ciphersuite, signing_key, responder_dh_public_key)
.map_err(|e| LpError::KKTError(e.to_string()))?;
Ok((session_secret, context, KKTRequestData(request_bytes)))
}
/// Processes a KKT response and extracts the responder's KEM public key.
///
/// This is called by the **client (initiator)** after receiving a KKT response
/// from the gateway. It verifies the signature and validates the key hash.
///
/// # Arguments
/// * `context` - Context from the initial `create_request()` call
/// * `session_secret` - The KKT session secret key from the initial `create_request()` call
/// * `responder_vk` - Responder's Ed25519 verification key (from directory)
/// * `expected_key_hash` - Expected hash of responder's KEM key (from directory)
/// * `response_data` - Serialized KKT response frame from responder
///
/// # Returns
/// * `EncapsulationKey` - Authenticated KEM public key of the responder
///
/// # Errors
/// Returns `LpError::KKTError` if:
/// - Response deserialization fails
/// - Signature verification fails
/// - Key hash doesn't match expected value
pub fn process_response<'a>(
mut context: KKTContext,
session_secret: &KKTSessionSecret,
responder_vk: &ed25519::PublicKey,
expected_key_hash: &[u8],
response_data: &KKTResponseData,
) -> Result<EncapsulationKey<'a>, LpError> {
validate_kem_response(
&mut context,
session_secret,
responder_vk,
expected_key_hash,
&response_data.0,
)
.map_err(|e| LpError::KKTError(e.to_string()))
}
/// Handles a KKT request and generates a signed response with the responder's KEM key.
///
/// This is called by the **gateway (responder)** when receiving a KKT request
/// from a client. It validates the request signature (if authenticated) and
/// responds with the gateway's KEM public key, signed for authenticity.
///
/// # Arguments
/// * `request_data` - Serialized KKT request frame from initiator
/// * `initiator_vk` - Initiator's Ed25519 verification key (None for anonymous)
/// * `responder_signing_key` - Gateway's Ed25519 signing key
/// * `responder_dh_private_key` - Gateway's x25519 private key
/// * `responder_kem_key` - Gateway's KEM public key to send
///
/// # Returns
/// * `KKTResponseData` - Signed response frame containing the KEM public key
///
/// # Errors
/// Returns `LpError::KKTError` if:
/// - Request deserialization fails
/// - Signature verification fails (if authenticated)
/// - Response generation fails
pub fn handle_request<'a>(
request_data: &KKTRequestData,
initiator_vk: Option<&ed25519::PublicKey>,
responder_signing_key: &ed25519::PrivateKey,
responder_dh_private_key: &x25519::PrivateKey,
responder_kem_key: &EncapsulationKey<'a>,
) -> Result<KKTResponseData, LpError> {
let mut rng = rand09::rng();
// Handle the request and generate response
let response_bytes = handle_kem_request(
&mut rng,
&request_data.0,
initiator_vk,
responder_signing_key,
responder_dh_private_key,
responder_kem_key,
)
.map_err(|e| LpError::KKTError(e.to_string()))?;
Ok(KKTResponseData(response_bytes))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::peer::mock_peers;
use nym_kkt::ciphersuite::{HashFunction, KEM, SignatureScheme};
use nym_kkt::key_utils::{
generate_keypair_ed25519, generate_keypair_libcrux, generate_keypair_x25519,
hash_encapsulation_key,
};
use rand09::RngCore;
#[test]
fn test_kkt_roundtrip_authenticated() {
let mut rng = rand09::rng();
// Generate Ed25519 keypairs for both parties
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
let responder_x25519 = generate_keypair_x25519(&mut rng);
// Generate responder's KEM keypair (X25519 for testing)
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
// Create ciphersuite
let ciphersuite = Ciphersuite::resolve_ciphersuite(
KEM::X25519,
HashFunction::Blake3,
SignatureScheme::Ed25519,
None,
)
.unwrap();
// Hash the KEM key (simulating directory storage)
let key_hash = hash_encapsulation_key(
&ciphersuite.hash_function(),
ciphersuite.hash_len(),
&responder_kem_key.encode(),
);
// Client: Create request
let (session_secret, context, request_data) = create_request(
ciphersuite,
initiator_ed25519_keypair.private_key(),
responder_x25519.public_key(),
)
.unwrap();
// Gateway: Handle request
let response_data = handle_request(
&request_data,
Some(initiator_ed25519_keypair.public_key()),
responder_ed25519_keypair.private_key(),
responder_x25519.private_key(),
&responder_kem_key,
)
.unwrap();
// Client: Process response
let obtained_key = process_response(
context,
&session_secret,
responder_ed25519_keypair.public_key(),
&key_hash,
&response_data,
)
.unwrap();
// Verify we got the correct KEM key
assert_eq!(obtained_key.encode(), responder_kem_key.encode());
}
// #[test]
// fn test_kkt_roundtrip_anonymous() {
// let mut rng = rand09::rng();
// // Only responder has keys (anonymous initiator)
// // Generate Ed25519 keypairs for both parties
// let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
// let responder_x25519 = generate_keypair_x25519(&mut rng);
// let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
// let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
// let ciphersuite = Ciphersuite::resolve_ciphersuite(
// KEM::X25519,
// HashFunction::Blake3,
// SignatureScheme::Ed25519,
// None,
// )
// .unwrap();
// let key_hash = hash_encapsulation_key(
// &ciphersuite.hash_function(),
// ciphersuite.hash_len(),
// &responder_kem_key.encode(),
// );
// // Anonymous initiator - use anonymous_initiator_process directly
// use nym_kkt::kkt::anonymous_initiator_process;
// let (mut context, request_frame) =
// anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
// let request_data = KKTRequestData(request_frame.to_bytes());
// // Gateway: Handle anonymous request
// let response_data = handle_request(
// &request_data,
// None,
// responder_ed25519_keypair.private_key(),
// &responder_x25519_sk,
// &responder_kem_key,
// )
// .unwrap();
// // Initiator: Validate response
// let obtained_key = initiator_ingest_response(
// &mut context,
// responder_ed25519_keypair.public_key(),
// &key_hash,
// &response_data.0,
// )
// .unwrap();
// assert_eq!(obtained_key.encode(), responder_kem_key.encode());
// }
#[test]
fn test_invalid_signature_rejected() {
let mut rng = rand09::rng();
// Generate Ed25519 keypairs for both parties
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
let responder_x25519 = generate_keypair_x25519(&mut rng);
// Different keypair for wrong signature
let mut wrong_secret = [0u8; 32];
rng.fill_bytes(&mut wrong_secret);
let wrong_keypair = ed25519::KeyPair::from_secret(wrong_secret, 2);
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
let ciphersuite = Ciphersuite::resolve_ciphersuite(
KEM::X25519,
HashFunction::Blake3,
SignatureScheme::Ed25519,
None,
)
.unwrap();
let (_session_secret, _context, request_data) = create_request(
ciphersuite,
initiator_ed25519_keypair.private_key(),
responder_x25519.public_key(),
)
.unwrap();
// Gateway handles request but we provide WRONG verification key
let result = handle_request(
&request_data,
Some(wrong_keypair.public_key()), // Wrong key!
responder_ed25519_keypair.private_key(),
responder_x25519.private_key(),
&responder_kem_key,
);
// Should fail signature verification
assert!(result.is_err());
if let Err(LpError::KKTError(_)) = result {
// Expected
} else {
panic!("Expected KKTError");
}
}
#[test]
fn test_hash_mismatch_rejected() {
let (init, resp) = mock_peers();
let responder_kem_key = resp.encapsulate_kem_key().unwrap();
let ciphersuite = Ciphersuite::resolve_ciphersuite(
KEM::X25519,
HashFunction::Blake3,
SignatureScheme::Ed25519,
None,
)
.unwrap();
// Use WRONG hash
let wrong_hash = [0u8; 32];
let (session_secret, context, request_data) = create_request(
ciphersuite,
init.ed25519.private_key(),
resp.x25519.public_key(),
)
.unwrap();
let response_data = handle_request(
&request_data,
Some(init.ed25519.public_key()),
resp.ed25519.private_key(),
resp.x25519.private_key(),
&responder_kem_key,
)
.unwrap();
// Client validates with WRONG hash
let result = process_response(
context,
&session_secret,
resp.ed25519.public_key(),
&wrong_hash, // Wrong!
&response_data,
);
// Should fail hash validation
assert!(result.is_err());
if let Err(LpError::KKTError(_)) = result {
// Expected
} else {
panic!("Expected KKTError");
}
}
#[test]
fn test_malformed_request_rejected() {
let mut rng = rand09::rng();
let mut responder_secret = [0u8; 32];
rng.fill_bytes(&mut responder_secret);
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
let responder_x25519 = generate_keypair_x25519(&mut rng);
let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk);
// Create malformed request data (invalid bytes)
let malformed_request = KKTRequestData(vec![0xFF; 100]);
let result = handle_request(
&malformed_request,
None,
responder_ed25519_keypair.private_key(),
responder_x25519.private_key(),
&responder_kem_key,
);
// Should fail to parse
assert!(result.is_err());
if let Err(LpError::KKTError(_)) = result {
// Expected
} else {
panic!("Expected KKTError");
}
}
#[test]
fn test_malformed_response_rejected() {
let mut rng = rand09::rng();
// Generate Ed25519 keypairs for both parties
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1));
let responder_x25519 = generate_keypair_x25519(&mut rng);
let ciphersuite = Ciphersuite::resolve_ciphersuite(
KEM::X25519,
HashFunction::Blake3,
SignatureScheme::Ed25519,
None,
)
.unwrap();
let (session_secret, context, _request_data) = create_request(
ciphersuite,
initiator_ed25519_keypair.private_key(),
responder_x25519.public_key(),
)
.unwrap();
// Create malformed response data
let malformed_response = KKTResponseData(vec![0xFF; 100]);
let key_hash = [0u8; 32];
let result = process_response(
context,
&session_secret,
responder_ed25519_keypair.public_key(),
&key_hash,
&malformed_response,
);
// Should fail to parse
assert!(result.is_err());
if let Err(LpError::KKTError(_)) = result {
// Expected
} else {
panic!("Expected KKTError");
}
}
}
+102 -109
View File
@@ -7,13 +7,9 @@ pub mod codec;
// pub use config::LpConfig;
pub mod error;
// georgio: no use for this
// pub mod kkt_orchestrator;
pub mod message;
pub mod noise_protocol;
pub mod packet;
pub mod peer;
pub mod psk;
pub mod psq;
pub mod replay;
pub mod session;
@@ -22,17 +18,13 @@ pub mod session_manager;
pub mod state_machine;
pub use error::LpError;
pub use message::{ClientHelloData, LpMessage};
pub use packet::{BOOTSTRAP_RECEIVER_IDX, LpPacket, OuterHeader};
pub use message::LpMessage;
pub use packet::{LpPacket, OuterHeader};
pub use replay::{ReceivingKeyCounterValidator, ReplayError};
pub use session::LpSession;
pub use session_manager::SessionManager;
pub use state_machine::LpStateMachine;
// noiserm
pub const NOISE_PATTERN: &str = "Noise_XKpsk3_25519_ChaChaPoly_SHA256";
pub const NOISE_PSK_INDEX: u8 = 3;
#[cfg(test)]
pub fn kem_list() -> Vec<nym_kkt_ciphersuite::KEM> {
todo!()
@@ -179,108 +171,109 @@ mod tests {
use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, SignatureScheme};
// Import the new standalone functions
use crate::codec::{parse_lp_packet, serialize_lp_packet};
use crate::codec::serialize_lp_packet;
#[test]
fn test_replay_protection_integration() {
for kem in kem_list() {
// Create session
let mut session = mock_session_for_test();
// === Packet 1 (Counter 0 - Should succeed) ===
let packet1 = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 42, // Matches session's sending_index assumption for this test
counter: 0,
},
message: LpMessage::Busy,
trailer: [0u8; TRAILER_LEN],
};
// Serialize packet
let mut buf1 = BytesMut::new();
serialize_lp_packet(&packet1, &mut buf1, None).unwrap();
// Parse packet
let parsed_packet1 = parse_lp_packet(&buf1, None).unwrap();
// Perform replay check (should pass)
session
.receiving_counter_quick_check(parsed_packet1.header.counter)
.expect("Initial packet failed replay check");
// Mark received (simulating successful processing)
session
.receiving_counter_mark(parsed_packet1.header.counter)
.expect("Failed to mark initial packet received");
// === Packet 2 (Counter 0 - Replay, should fail check) ===
let packet2 = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 42,
counter: 0, // Same counter as before (replay)
},
message: LpMessage::Busy,
trailer: [0u8; TRAILER_LEN],
};
// Serialize packet
let mut buf2 = BytesMut::new();
serialize_lp_packet(&packet2, &mut buf2, None).unwrap();
// Parse packet
let parsed_packet2 = parse_lp_packet(&buf2, None).unwrap();
// Perform replay check (should fail)
let replay_result =
session.receiving_counter_quick_check(parsed_packet2.header.counter);
assert!(replay_result.is_err());
match replay_result.unwrap_err() {
LpError::Replay(e) => {
assert!(matches!(e, crate::replay::ReplayError::DuplicateCounter));
}
e => panic!("Expected replay error, got {:?}", e),
}
// Do not mark received as it failed validation
// === Packet 3 (Counter 1 - Should succeed) ===
let packet3 = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 42,
counter: 1, // Incremented counter
},
message: LpMessage::Busy,
trailer: [0u8; TRAILER_LEN],
};
// Serialize packet
let mut buf3 = BytesMut::new();
serialize_lp_packet(&packet3, &mut buf3, None).unwrap();
// Parse packet
let parsed_packet3 = parse_lp_packet(&buf3, None).unwrap();
// Perform replay check (should pass)
session
.receiving_counter_quick_check(parsed_packet3.header.counter)
.expect("Packet 3 failed replay check");
// Mark received
session
.receiving_counter_mark(parsed_packet3.header.counter)
.expect("Failed to mark packet 3 received");
// Verify validator state directly on the session
let state = session.current_packet_cnt();
assert_eq!(state.0, 2); // Next expected counter (correct - was 1, now expects 2)
assert_eq!(state.1, 2); // Total marked received (correct - packets 1 and 3)
}
todo!()
// for kem in kem_list() {
// // Create session
// let mut session = mock_session_for_test();
//
// // === Packet 1 (Counter 0 - Should succeed) ===
// let packet1 = LpPacket {
// header: LpHeader {
// protocol_version: 1,
// reserved: [0u8; 3],
// receiver_idx: 42, // Matches session's sending_index assumption for this test
// counter: 0,
// },
// message: LpMessage::Busy,
// trailer: [0u8; TRAILER_LEN],
// };
//
// // Serialize packet
// let mut buf1 = BytesMut::new();
// serialize_lp_packet(&packet1, &mut buf1, None).unwrap();
//
// // Parse packet
// let parsed_packet1 = parse_lp_packet(&buf1, None).unwrap();
//
// // Perform replay check (should pass)
// session
// .receiving_counter_quick_check(parsed_packet1.header.counter)
// .expect("Initial packet failed replay check");
//
// // Mark received (simulating successful processing)
// session
// .receiving_counter_mark(parsed_packet1.header.counter)
// .expect("Failed to mark initial packet received");
//
// // === Packet 2 (Counter 0 - Replay, should fail check) ===
// let packet2 = LpPacket {
// header: LpHeader {
// protocol_version: 1,
// reserved: [0u8; 3],
// receiver_idx: 42,
// counter: 0, // Same counter as before (replay)
// },
// message: LpMessage::Busy,
// trailer: [0u8; TRAILER_LEN],
// };
//
// // Serialize packet
// let mut buf2 = BytesMut::new();
// serialize_lp_packet(&packet2, &mut buf2, None).unwrap();
//
// // Parse packet
// let parsed_packet2 = parse_lp_packet(&buf2, None).unwrap();
//
// // Perform replay check (should fail)
// let replay_result =
// session.receiving_counter_quick_check(parsed_packet2.header.counter);
// assert!(replay_result.is_err());
// match replay_result.unwrap_err() {
// LpError::Replay(e) => {
// assert!(matches!(e, crate::replay::ReplayError::DuplicateCounter));
// }
// e => panic!("Expected replay error, got {:?}", e),
// }
// // Do not mark received as it failed validation
//
// // === Packet 3 (Counter 1 - Should succeed) ===
// let packet3 = LpPacket {
// header: LpHeader {
// protocol_version: 1,
// reserved: [0u8; 3],
// receiver_idx: 42,
// counter: 1, // Incremented counter
// },
// message: LpMessage::Busy,
// trailer: [0u8; TRAILER_LEN],
// };
//
// // Serialize packet
// let mut buf3 = BytesMut::new();
// serialize_lp_packet(&packet3, &mut buf3, None).unwrap();
//
// // Parse packet
// let parsed_packet3 = parse_lp_packet(&buf3, None).unwrap();
//
// // Perform replay check (should pass)
// session
// .receiving_counter_quick_check(parsed_packet3.header.counter)
// .expect("Packet 3 failed replay check");
//
// // Mark received
// session
// .receiving_counter_mark(parsed_packet3.header.counter)
// .expect("Failed to mark packet 3 received");
//
// // Verify validator state directly on the session
// let state = session.current_packet_cnt();
// assert_eq!(state.0, 2); // Next expected counter (correct - was 1, now expects 2)
// assert_eq!(state.1, 2); // Total marked received (correct - packets 1 and 3)
// }
}
#[test]
+71 -597
View File
@@ -1,165 +1,31 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::packet::LpHeader;
use crate::peer::LpRemotePeer;
use crate::{BOOTSTRAP_RECEIVER_IDX, LpError, LpPacket};
use crate::LpError;
use bytes::{BufMut, BytesMut};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use nym_crypto::asymmetric::{ed25519, x25519};
use rand::RngCore;
use nym_crypto::asymmetric::ed25519;
use std::fmt::{self, Display};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
/// Data structure for the ClientHello message
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ClientHelloData {
/// Client-proposed receiver index for session identification (4 bytes)
/// Auto-generated randomly by the client
pub receiver_index: u32,
/// Client's LP x25519 public key (32 bytes) - derived from Ed25519 key
pub client_lp_public_key: x25519::PublicKey,
/// Client's Ed25519 public key (32 bytes) - for PSQ authentication
pub client_ed25519_public_key: ed25519::PublicKey,
// noiserm
/// Salt for PSK derivation (32 bytes: 8-byte timestamp + 24-byte nonce)
pub salt: [u8; 32],
}
impl ClientHelloData {
// noiserm (remove 32 bytes for salt)
// 4 bytes for receiver index + 32 bytes for client lp key, 32 bytes for client ed25519 key + 32 bytes for salt
pub const LEN: usize = 100;
pub fn into_lp_packet(self, protocol_version: u8) -> LpPacket {
LpPacket::new(
LpHeader::new(
BOOTSTRAP_RECEIVER_IDX, // session_id not yet established
0, // counter starts at 0
protocol_version,
),
LpMessage::ClientHello(self),
)
}
fn len(&self) -> usize {
Self::LEN
}
fn generate_receiver_index() -> u32 {
loop {
let candidate = rand::random();
if candidate != BOOTSTRAP_RECEIVER_IDX {
return candidate;
}
}
}
// noiserm
/// 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 (derived from Ed25519)
/// * `client_ed25519_public_key` - Client's Ed25519 public key (for PSQ authentication)
pub fn new_with_fresh_salt(
client_lp_public_key: x25519::PublicKey,
client_ed25519_public_key: ed25519::PublicKey,
timestamp: u64,
) -> Self {
// Generate salt: timestamp + nonce
let mut salt = [0u8; 32];
// First 8 bytes: current timestamp as u64 little-endian
salt[..8].copy_from_slice(&timestamp.to_le_bytes());
// Last 24 bytes: random nonce
rand::thread_rng().fill_bytes(&mut salt[8..]);
Self {
receiver_index: Self::generate_receiver_index(), // Auto-generate random receiver index
client_lp_public_key,
client_ed25519_public_key,
salt,
}
}
// noiserm
/// 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)
}
pub fn encode(&self, dst: &mut BytesMut) {
dst.put_u32_le(self.receiver_index);
dst.put_slice(self.client_lp_public_key.as_bytes());
dst.put_slice(self.client_ed25519_public_key.as_bytes());
// noiserm
dst.put_slice(&self.salt);
}
pub fn decode(b: &[u8]) -> Result<Self, LpError> {
if b.len() != Self::LEN {
return Err(LpError::DeserializationError(format!(
"Expected {} bytes to deserialise ClientHelloData. got {}",
Self::LEN,
b.len()
)));
}
// SAFETY: we checked for valid byte lengths
#[allow(clippy::unwrap_used)]
let client_lp_public_key_bytes = b[4..36].try_into().unwrap();
let client_ed25519_public_key_bytes = b[36..68].try_into().unwrap();
Ok(ClientHelloData {
receiver_index: u32::from_le_bytes([b[0], b[1], b[2], b[3]]),
client_lp_public_key: x25519::PublicKey::from_byte_array(client_lp_public_key_bytes),
client_ed25519_public_key: ed25519::PublicKey::from_byte_array(
client_ed25519_public_key_bytes,
)?,
// noiserm
salt: b[68..].try_into().unwrap(),
})
}
/// Attempt to construct remote peer information based on the data provided in this packet.
pub fn to_remote_peer(&self) -> LpRemotePeer {
LpRemotePeer::new(self.client_ed25519_public_key, self.client_lp_public_key)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)]
#[repr(u32)]
pub enum MessageType {
/// The party is busy
Busy = 0x0000,
Handshake = 0x0001,
EncryptedData = 0x0002,
ClientHello = 0x0003,
KKTRequest = 0x0004,
KKTResponse = 0x0005,
ForwardPacket = 0x0006,
/// Receiver index collision - client should retry with new index
Collision = 0x0007,
/// Acknowledgment - gateway confirms receipt of message
Ack = 0x0008,
/// Subsession request - client initiates subsession creation
SubsessionRequest = 0x0009,
// georgio: this should be the psq msg
/// Subsession KK1 - first message of Noise KK handshake
SubsessionKK1 = 0x000A,
/// Subsession KK2 - second message of Noise KK handshake
SubsessionKK2 = 0x000B,
/// Subsession ready - subsession established confirmation
SubsessionReady = 0x000C,
/// Subsession abort - race winner tells loser to become responder
SubsessionAbort = 0x000D,
/// Encrypted payload
EncryptedData = 0x0001,
/// Receiver should forward this message via telescoping
ForwardPacket = 0x0002,
/// Receiver index collision - client should retry with new index
Collision = 0x0003,
/// Acknowledgment - gateway confirms receipt of message
Ack = 0x0004,
/// General error
Error = 0x00FF,
}
@@ -175,31 +41,10 @@ impl MessageType {
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HandshakeData(pub Vec<u8>);
pub struct ApplicationData(pub Vec<u8>);
impl HandshakeData {
pub(crate) fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
fn len(&self) -> usize {
self.0.len()
}
fn encode(&self, dst: &mut BytesMut) {
dst.put_slice(&self.0);
}
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
Ok(HandshakeData(bytes.to_vec()))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncryptedDataPayload(pub Vec<u8>);
impl EncryptedDataPayload {
#[allow(dead_code)]
pub(crate) fn new(bytes: Vec<u8>) -> Self {
impl ApplicationData {
pub fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
@@ -212,51 +57,7 @@ impl EncryptedDataPayload {
}
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
Ok(EncryptedDataPayload(bytes.to_vec()))
}
}
/// KKT request frame data (serialized KKTFrame bytes)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KKTRequestData(pub Vec<u8>);
impl KKTRequestData {
pub(crate) fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
fn len(&self) -> usize {
self.0.len()
}
fn encode(&self, dst: &mut BytesMut) {
dst.put_slice(&self.0);
}
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
Ok(KKTRequestData(bytes.to_vec()))
}
}
/// KKT response frame data (serialized KKTFrame bytes)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KKTResponseData(pub Vec<u8>);
impl KKTResponseData {
pub(crate) fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
fn len(&self) -> usize {
self.0.len()
}
fn encode(&self, dst: &mut BytesMut) {
dst.put_slice(&self.0);
}
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
Ok(KKTResponseData(bytes.to_vec()))
Ok(ApplicationData(bytes.to_vec()))
}
}
@@ -306,42 +107,6 @@ impl ErrorPacketData {
}
}
/// PSQ request frame data (serialized bytes)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PSQRequestData(pub Vec<u8>);
impl PSQRequestData {
fn len(&self) -> usize {
self.0.len()
}
fn encode(&self, dst: &mut BytesMut) {
dst.put_slice(&self.0);
}
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
Ok(PSQRequestData(bytes.to_vec()))
}
}
/// PSQ response frame data (serialized bytes)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PSQResponseData(pub Vec<u8>);
impl PSQResponseData {
fn len(&self) -> usize {
self.0.len()
}
fn encode(&self, dst: &mut BytesMut) {
dst.put_slice(&self.0);
}
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
Ok(PSQResponseData(bytes.to_vec()))
}
}
/// Packet forwarding request with embedded inner LP packet
#[derive(Debug, Clone)]
pub struct ForwardPacketData {
@@ -467,146 +232,30 @@ impl ForwardPacketData {
}
}
// georgio: swap with psq
/// Subsession KK1 message - first message of Noise KK handshake
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubsessionKK1Data {
/// Noise KK first message payload (ephemeral key + encrypted static)
pub payload: Vec<u8>,
}
impl SubsessionKK1Data {
fn len(&self) -> usize {
self.payload.len()
}
fn encode(&self, dst: &mut BytesMut) {
dst.put_slice(&self.payload);
}
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
Ok(SubsessionKK1Data {
payload: bytes.to_vec(),
})
}
}
/// Subsession KK2 message - second message of Noise KK handshake
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubsessionKK2Data {
/// Noise KK second message payload (ephemeral key + encrypted response)
pub payload: Vec<u8>,
}
impl SubsessionKK2Data {
fn len(&self) -> usize {
self.payload.len()
}
fn encode(&self, dst: &mut BytesMut) {
dst.put_slice(&self.payload);
}
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
Ok(SubsessionKK2Data {
payload: bytes.to_vec(),
})
}
}
/// Subsession ready confirmation with new session index
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubsessionReadyData {
/// New subsession's receiver index for routing
pub receiver_index: u32,
}
impl SubsessionReadyData {
pub const LEN: usize = 4;
fn len(&self) -> usize {
Self::LEN
}
fn encode(&self, dst: &mut BytesMut) {
dst.put_u32_le(self.receiver_index);
}
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
if bytes.len() != 4 {
return Err(LpError::DeserializationError(format!(
"Expected 4 bytes to deserialise SubsessionReadyData. got {}",
bytes.len()
)));
}
Ok(SubsessionReadyData {
receiver_index: u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
})
}
}
#[derive(Debug, Clone)]
pub enum LpMessage {
/// The party is busy
Busy,
PSQRequest(PSQRequestData),
PSQResponse(PSQResponseData),
EncryptedData(EncryptedDataPayload),
ClientHello(ClientHelloData),
KKTRequest(KKTRequestData),
KKTResponse(KKTResponseData),
/// Application payload is being sent
ApplicationData(ApplicationData),
/// Receiver should forward this message via telescoping
ForwardPacket(ForwardPacketData),
/// Receiver index collision - client should retry with new receiver_index
Collision,
/// Acknowledgment - gateway confirms receipt of message
Ack,
// georgio: this should become psq stuff
/// Subsession request - client initiates subsession creation (empty, signal only)
SubsessionRequest,
/// Subsession KK1 - first message of Noise KK handshake
SubsessionKK1(SubsessionKK1Data),
/// Subsession KK2 - second message of Noise KK handshake
SubsessionKK2(SubsessionKK2Data),
/// Subsession ready - subsession established confirmation
SubsessionReady(SubsessionReadyData),
/// Subsession abort - race winner tells loser to become responder (empty, signal only)
SubsessionAbort,
/// An error has occurred
Error(ErrorPacketData),
}
impl From<PSQRequestData> for LpMessage {
fn from(value: PSQRequestData) -> Self {
LpMessage::PSQRequest(value)
}
}
impl From<PSQResponseData> for LpMessage {
fn from(value: PSQResponseData) -> Self {
LpMessage::PSQResponse(value)
}
}
impl From<EncryptedDataPayload> for LpMessage {
fn from(value: EncryptedDataPayload) -> Self {
LpMessage::EncryptedData(value)
}
}
impl From<ClientHelloData> for LpMessage {
fn from(value: ClientHelloData) -> Self {
LpMessage::ClientHello(value)
}
}
impl From<KKTRequestData> for LpMessage {
fn from(value: KKTRequestData) -> Self {
LpMessage::KKTRequest(value)
}
}
impl From<KKTResponseData> for LpMessage {
fn from(value: KKTResponseData) -> Self {
LpMessage::KKTResponse(value)
impl From<ApplicationData> for LpMessage {
fn from(value: ApplicationData) -> Self {
LpMessage::ApplicationData(value)
}
}
@@ -616,86 +265,40 @@ impl From<ForwardPacketData> for LpMessage {
}
}
impl From<SubsessionKK1Data> for LpMessage {
fn from(value: SubsessionKK1Data) -> Self {
LpMessage::SubsessionKK1(value)
}
}
impl From<SubsessionKK2Data> for LpMessage {
fn from(value: SubsessionKK2Data) -> Self {
LpMessage::SubsessionKK2(value)
}
}
impl From<SubsessionReadyData> for LpMessage {
fn from(value: SubsessionReadyData) -> Self {
LpMessage::SubsessionReady(value)
}
}
impl Display for LpMessage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LpMessage::Busy => write!(f, "Busy"),
LpMessage::EncryptedData(_) => write!(f, "EncryptedData"),
LpMessage::ClientHello(_) => write!(f, "ClientHello"),
LpMessage::KKTRequest(_) => write!(f, "KKTRequest"),
LpMessage::KKTResponse(_) => write!(f, "KKTResponse"),
LpMessage::ApplicationData(_) => write!(f, "EncryptedData"),
LpMessage::ForwardPacket(_) => write!(f, "ForwardPacket"),
LpMessage::Collision => write!(f, "Collision"),
LpMessage::Ack => write!(f, "Ack"),
LpMessage::SubsessionRequest => write!(f, "SubsessionRequest"),
LpMessage::SubsessionKK1(_) => write!(f, "SubsessionKK1"),
LpMessage::SubsessionKK2(_) => write!(f, "SubsessionKK2"),
LpMessage::SubsessionReady(_) => write!(f, "SubsessionReady"),
LpMessage::SubsessionAbort => write!(f, "SubsessionAbort"),
LpMessage::Error(_) => write!(f, "Error"),
LpMessage::PSQRequest(_) => write!(f, "PSQRequest"),
LpMessage::PSQResponse(_) => write!(f, "PSQResponse"),
}
}
}
impl LpMessage {
#[deprecated(note = "is it actually needed?")]
pub fn payload(&self) -> &[u8] {
match self {
LpMessage::Busy => &[],
LpMessage::PSQRequest(payload) => payload.0.as_slice(),
LpMessage::PSQResponse(payload) => payload.0.as_slice(),
LpMessage::EncryptedData(payload) => payload.0.as_slice(),
LpMessage::ClientHello(_) => &[], // Structured data, serialized in encode_content
LpMessage::KKTRequest(payload) => payload.0.as_slice(),
LpMessage::KKTResponse(payload) => payload.0.as_slice(),
LpMessage::ApplicationData(payload) => payload.0.as_slice(),
LpMessage::ForwardPacket(_) => &[], // Structured data, serialized in encode_content
LpMessage::Collision => &[],
LpMessage::Ack => &[],
LpMessage::SubsessionRequest => &[],
LpMessage::SubsessionKK1(_) => &[], // Structured data, serialized in encode_content
LpMessage::SubsessionKK2(_) => &[], // Structured data, serialized in encode_content
LpMessage::SubsessionReady(_) => &[], // Structured data, serialized in encode_content
LpMessage::SubsessionAbort => &[],
LpMessage::Error(_) => &[], // Structured data, serialized in encode_content (?)
}
}
#[deprecated(note = "is it actually needed?")]
pub fn is_empty(&self) -> bool {
match self {
LpMessage::Busy => true,
LpMessage::EncryptedData(payload) => payload.0.is_empty(),
LpMessage::ClientHello(_) => false, // Always has data
LpMessage::KKTRequest(payload) => payload.0.is_empty(),
LpMessage::KKTResponse(payload) => payload.0.is_empty(),
LpMessage::ApplicationData(payload) => payload.0.is_empty(),
LpMessage::ForwardPacket(_) => false, // Always has data
LpMessage::Collision => true,
LpMessage::Ack => true,
LpMessage::SubsessionRequest => true, // Empty signal
LpMessage::SubsessionKK1(_) => false, // Always has payload
LpMessage::SubsessionKK2(_) => false, // Always has payload
LpMessage::SubsessionReady(_) => false, // Always has receiver_index
LpMessage::SubsessionAbort => true, // Empty signal
LpMessage::PSQRequest(_) => true, // Always had data (?)
LpMessage::PSQResponse(_) => true, // Always had data (?)
LpMessage::Error(_) => false,
}
}
@@ -703,20 +306,10 @@ impl LpMessage {
pub fn len(&self) -> usize {
match self {
LpMessage::Busy => 0,
LpMessage::PSQRequest(payload) => payload.len(),
LpMessage::PSQResponse(payload) => payload.len(),
LpMessage::EncryptedData(payload) => payload.len(),
LpMessage::ClientHello(payload) => payload.len(),
LpMessage::KKTRequest(payload) => payload.len(),
LpMessage::KKTResponse(payload) => payload.len(),
LpMessage::ApplicationData(payload) => payload.len(),
LpMessage::ForwardPacket(payload) => payload.len(),
LpMessage::Collision => 0,
LpMessage::Ack => 0,
LpMessage::SubsessionRequest => 0,
LpMessage::SubsessionKK1(payload) => payload.len(),
LpMessage::SubsessionKK2(payload) => payload.len(),
LpMessage::SubsessionReady(payload) => payload.len(),
LpMessage::SubsessionAbort => 0,
LpMessage::Error(payload) => payload.len(),
}
}
@@ -724,20 +317,10 @@ impl LpMessage {
pub fn typ(&self) -> MessageType {
match self {
LpMessage::Busy => MessageType::Busy,
LpMessage::PSQRequest(_) => todo!(),
LpMessage::PSQResponse(_) => todo!(),
LpMessage::EncryptedData(_) => MessageType::EncryptedData,
LpMessage::ClientHello(_) => MessageType::ClientHello,
LpMessage::KKTRequest(_) => MessageType::KKTRequest,
LpMessage::KKTResponse(_) => MessageType::KKTResponse,
LpMessage::ApplicationData(_) => MessageType::EncryptedData,
LpMessage::ForwardPacket(_) => MessageType::ForwardPacket,
LpMessage::Collision => MessageType::Collision,
LpMessage::Ack => MessageType::Ack,
LpMessage::SubsessionRequest => MessageType::SubsessionRequest,
LpMessage::SubsessionKK1(_) => MessageType::SubsessionKK1,
LpMessage::SubsessionKK2(_) => MessageType::SubsessionKK2,
LpMessage::SubsessionReady(_) => MessageType::SubsessionReady,
LpMessage::SubsessionAbort => MessageType::SubsessionAbort,
LpMessage::Error(_) => MessageType::Error,
}
}
@@ -745,20 +328,10 @@ impl LpMessage {
pub fn encode_content(&self, dst: &mut BytesMut) {
match self {
LpMessage::Busy => { /* No content */ }
LpMessage::PSQRequest(payload) => payload.encode(dst),
LpMessage::PSQResponse(payload) => payload.encode(dst),
LpMessage::EncryptedData(payload) => payload.encode(dst),
LpMessage::ClientHello(data) => data.encode(dst),
LpMessage::KKTRequest(payload) => payload.encode(dst),
LpMessage::KKTResponse(payload) => payload.encode(dst),
LpMessage::ApplicationData(payload) => payload.encode(dst),
LpMessage::ForwardPacket(data) => data.encode(dst),
LpMessage::Collision => { /* No content */ }
LpMessage::Ack => { /* No content */ }
LpMessage::SubsessionRequest => { /* No content - signal only */ }
LpMessage::SubsessionKK1(data) => data.encode(dst),
LpMessage::SubsessionKK2(data) => data.encode(dst),
LpMessage::SubsessionReady(data) => data.encode(dst),
LpMessage::SubsessionAbort => { /* No content - signal only */ }
LpMessage::Error(data) => data.encode(dst),
}
}
@@ -773,17 +346,9 @@ impl LpMessage {
content.ensure_empty()?;
Ok(LpMessage::Busy)
}
MessageType::Handshake => todo!(),
MessageType::EncryptedData => Ok(LpMessage::EncryptedData(
EncryptedDataPayload::decode(content)?,
)),
MessageType::ClientHello => {
Ok(LpMessage::ClientHello(ClientHelloData::decode(content)?))
}
MessageType::KKTRequest => Ok(LpMessage::KKTRequest(KKTRequestData::decode(content)?)),
MessageType::KKTResponse => {
Ok(LpMessage::KKTResponse(KKTResponseData::decode(content)?))
}
MessageType::EncryptedData => Ok(LpMessage::ApplicationData(ApplicationData::decode(
content,
)?)),
MessageType::ForwardPacket => Ok(LpMessage::ForwardPacket(ForwardPacketData::decode(
content,
)?)),
@@ -795,23 +360,6 @@ impl LpMessage {
content.ensure_empty()?;
Ok(LpMessage::Ack)
}
MessageType::SubsessionRequest => {
content.ensure_empty()?;
Ok(LpMessage::SubsessionRequest)
}
MessageType::SubsessionKK1 => Ok(LpMessage::SubsessionKK1(SubsessionKK1Data::decode(
content,
)?)),
MessageType::SubsessionKK2 => Ok(LpMessage::SubsessionKK2(SubsessionKK2Data::decode(
content,
)?)),
MessageType::SubsessionReady => Ok(LpMessage::SubsessionReady(
SubsessionReadyData::decode(content)?,
)),
MessageType::SubsessionAbort => {
content.ensure_empty()?;
Ok(LpMessage::SubsessionAbort)
}
MessageType::Error => Ok(LpMessage::Error(ErrorPacketData::decode(content)?)),
}
}
@@ -836,114 +384,40 @@ impl EnsureEmptyContent for &[u8] {
#[cfg(test)]
mod tests {
use std::time::{SystemTime, UNIX_EPOCH};
use super::*;
use crate::LpPacket;
use crate::packet::{LpHeader, TRAILER_LEN};
#[test]
fn encoding() {
let message = LpMessage::EncryptedData(EncryptedDataPayload(vec![11u8; 124]));
let resp_header = LpHeader {
protocol_version: 1,
reserved: [0u8; 3],
receiver_idx: 0,
counter: 0,
};
let packet = LpPacket {
header: resp_header,
message,
trailer: [80; TRAILER_LEN],
};
// Just print packet for debug, will be captured in test output
println!("{packet:?}");
// Verify message type
assert!(matches!(packet.message.typ(), MessageType::EncryptedData));
// Verify correct data in message
match &packet.message {
LpMessage::EncryptedData(data) => {
assert_eq!(*data, EncryptedDataPayload(vec![11u8; 124]));
}
_ => panic!("Wrong message type"),
}
}
#[test]
fn test_client_hello_salt_generation() {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("System time before UNIX epoch")
.as_secs();
let mut rng = rand::thread_rng();
let ed25519 = ed25519::KeyPair::new(&mut rng);
let x25519 = ed25519.to_x25519();
let client_key = *x25519.public_key();
let client_ed25519_key = *ed25519.public_key();
let hello1 =
ClientHelloData::new_with_fresh_salt(client_key, client_ed25519_key, timestamp);
let hello2 =
ClientHelloData::new_with_fresh_salt(client_key, client_ed25519_key, timestamp);
// 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 timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("System time before UNIX epoch")
.as_secs();
let mut rng = rand::thread_rng();
let ed25519 = ed25519::KeyPair::new(&mut rng);
let x25519 = ed25519.to_x25519();
let client_key = *x25519.public_key();
let client_ed25519_key = *ed25519.public_key();
let hello = ClientHelloData::new_with_fresh_salt(client_key, client_ed25519_key, timestamp);
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 timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("System time before UNIX epoch")
.as_secs();
let mut rng = rand::thread_rng();
let ed25519 = ed25519::KeyPair::new(&mut rng);
let x25519 = ed25519.to_x25519();
let client_key = *x25519.public_key();
let client_ed25519_key = *ed25519.public_key();
let hello = ClientHelloData::new_with_fresh_salt(client_key, client_ed25519_key, timestamp);
// 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);
todo!()
// let message = LpMessage::EncryptedData(EncryptedDataPayload(vec![11u8; 124]));
//
// let resp_header = LpHeader {
// protocol_version: 1,
// reserved: [0u8; 3],
// receiver_idx: 0,
// counter: 0,
// };
//
// let packet = LpPacket {
// header: resp_header,
// message,
// trailer: [80; TRAILER_LEN],
// };
//
// // Just print packet for debug, will be captured in test output
// println!("{packet:?}");
//
// // Verify message type
// assert!(matches!(packet.message.typ(), MessageType::EncryptedData));
//
// // Verify correct data in message
// match &packet.message {
// LpMessage::EncryptedData(data) => {
// assert_eq!(*data, EncryptedDataPayload(vec![11u8; 124]));
// }
// _ => panic!("Wrong message type"),
// }
}
}
-337
View File
@@ -1,337 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Sans-IO Noise protocol state machine, adapted from noise-psq.
use snow::{TransportState, params::NoiseParams};
use thiserror::Error;
// --- Error Definition ---
/// Errors related to the Noise protocol state machine.
#[derive(Error, Debug)]
pub enum NoiseError {
#[error("encountered a Noise decryption error")]
DecryptionError,
#[error("encountered a Noise Protocol error - {0}")]
ProtocolError(snow::Error),
#[error("operation is invalid in the current protocol state")]
IncorrectStateError,
#[error("attempted transport mode operation without real PSK injection")]
PskNotInjected,
#[error("Other Noise-related error: {0}")]
Other(String),
#[error("session is read-only after demotion")]
SessionReadOnly,
}
impl From<snow::Error> for NoiseError {
fn from(err: snow::Error) -> Self {
match err {
snow::Error::Decrypt => NoiseError::DecryptionError,
err => NoiseError::ProtocolError(err),
}
}
}
// --- Protocol State and Structs ---
/// Represents the possible states of the Noise protocol machine.
#[derive(Debug)]
pub enum NoiseProtocolState {
/// The protocol is currently performing the handshake.
/// Contains the Snow handshake state.
Handshaking(Box<snow::HandshakeState>),
/// The handshake is complete, and the protocol is in transport mode.
/// Contains the Snow transport state.
Transport(TransportState),
/// The protocol has encountered an unrecoverable error.
/// Stores the error description.
Failed(String),
}
/// The core sans-io Noise protocol state machine.
#[derive(Debug)]
pub struct NoiseProtocol {
state: NoiseProtocolState,
// We might need buffers for incoming/outgoing data later if we add internal buffering
// read_buffer: Vec<u8>,
// write_buffer: Vec<u8>,
}
/// Represents the outcome of processing received bytes via `read_message`.
#[derive(Debug, PartialEq)]
pub enum ReadResult {
/// A handshake or transport message was successfully processed, but yielded no application data
/// and did not complete the handshake.
NoOp,
/// A complete application data message was decrypted.
DecryptedData(Vec<u8>),
/// The handshake successfully completed during this read operation.
HandshakeComplete,
// NOTE: NeedMoreBytes variant removed as read_message expects full frames.
}
// --- Implementation ---
impl NoiseProtocol {
pub fn params() -> NoiseParams {
// SAFETY: the hardcoded pattern must be valid
// and if for some reason it was not, we MUST fail non-gracefully for there is no possible recovery
#[allow(clippy::unwrap_used)]
crate::NOISE_PATTERN.parse().unwrap()
}
/// Creates a new `NoiseProtocol` instance in the Handshaking state.
///
/// Takes an initialized `snow::HandshakeState` (e.g., from `snow::Builder`).
pub fn new(initial_state: snow::HandshakeState) -> Self {
NoiseProtocol {
state: NoiseProtocolState::Handshaking(Box::new(initial_state)),
}
}
fn prepare_handshake_state<'a>(
local_private_key: &'a [u8],
remote_public_key: &'a [u8],
psk: &'a [u8],
) -> snow::Builder<'a> {
let psk_index = crate::NOISE_PSK_INDEX;
let noise_params = NoiseProtocol::params();
snow::Builder::new(noise_params)
.local_private_key(local_private_key)
.remote_public_key(remote_public_key)
.psk(psk_index, psk)
}
/// Builds a new `NoiseProtocol` initiator instance with the provided local private key,
/// remote public key and psk
pub fn build_new_initiator(
local_private_key: &[u8],
remote_public_key: &[u8],
psk: &[u8],
) -> Result<Self, NoiseError> {
let handshake_state =
Self::prepare_handshake_state(local_private_key, remote_public_key, psk)
.build_initiator()?;
Ok(Self::new(handshake_state))
}
/// Builds a new `NoiseProtocol` responder instance with the provided local private key,
/// remote public key and psk
pub fn build_new_responder(
local_private_key: &[u8],
remote_public_key: &[u8],
psk: &[u8],
) -> Result<Self, NoiseError> {
let handshake_state =
Self::prepare_handshake_state(local_private_key, remote_public_key, psk)
.build_responder()?;
Ok(Self::new(handshake_state))
}
/// Processes a single, complete incoming Noise message frame.
///
/// Assumes the caller handles buffering and framing to provide one full message.
/// Returns the result of processing the message.
pub fn read_message(&mut self, input: &[u8]) -> Result<ReadResult, NoiseError> {
// Allocate a buffer large enough for the maximum possible Noise message size.
// TODO: Consider reusing a buffer for efficiency.
let mut buffer = vec![0u8; 65535]; // Max Noise message size
match &mut self.state {
NoiseProtocolState::Handshaking(handshake_state) => {
match handshake_state.read_message(input, &mut buffer) {
Ok(_) => {
if handshake_state.is_handshake_finished() {
// Transition to Transport state.
let current_state = std::mem::replace(
&mut self.state,
// Temporary placeholder needed for mem::replace
NoiseProtocolState::Failed(
NoiseError::IncorrectStateError.to_string(),
),
);
if let NoiseProtocolState::Handshaking(state_to_convert) = current_state
{
match state_to_convert.into_transport_mode() {
Ok(transport_state) => {
self.state = NoiseProtocolState::Transport(transport_state);
Ok(ReadResult::HandshakeComplete)
}
Err(e) => {
let err = NoiseError::from(e);
self.state = NoiseProtocolState::Failed(err.to_string());
Err(err)
}
}
} else {
// Should be unreachable
let err = NoiseError::IncorrectStateError;
self.state = NoiseProtocolState::Failed(err.to_string());
Err(err)
}
} else {
// Handshake continues
Ok(ReadResult::NoOp)
}
}
Err(e) => {
let err = NoiseError::from(e);
self.state = NoiseProtocolState::Failed(err.to_string());
Err(err)
}
}
}
NoiseProtocolState::Transport(transport_state) => {
match transport_state.read_message(input, &mut buffer) {
Ok(len) => Ok(ReadResult::DecryptedData(buffer[..len].to_vec())),
Err(e) => {
let err = NoiseError::from(e);
self.state = NoiseProtocolState::Failed(err.to_string());
Err(err)
}
}
}
NoiseProtocolState::Failed(_) => Err(NoiseError::IncorrectStateError),
}
}
/// Checks if there are pending handshake messages to send.
///
/// If in Handshaking state and it's our turn, generates the message.
/// Transitions state to Transport if the handshake completes after this message.
/// Returns `None` if not in Handshaking state or not our turn.
pub fn get_bytes_to_send(&mut self) -> Option<Result<Vec<u8>, NoiseError>> {
match &mut self.state {
NoiseProtocolState::Handshaking(handshake_state) => {
if handshake_state.is_my_turn() {
let mut buffer = vec![0u8; 65535];
match handshake_state.write_message(&[], &mut buffer) {
// Empty payload for handshake msg
Ok(len) => {
if handshake_state.is_handshake_finished() {
// Transition to Transport state.
let current_state = std::mem::replace(
&mut self.state,
NoiseProtocolState::Failed(
NoiseError::IncorrectStateError.to_string(),
),
);
if let NoiseProtocolState::Handshaking(state_to_convert) =
current_state
{
match state_to_convert.into_transport_mode() {
Ok(transport_state) => {
self.state =
NoiseProtocolState::Transport(transport_state);
Some(Ok(buffer[..len].to_vec())) // Return final handshake msg
}
Err(e) => {
let err = NoiseError::from(e);
self.state =
NoiseProtocolState::Failed(err.to_string());
Some(Err(err))
}
}
} else {
// Should be unreachable
let err = NoiseError::IncorrectStateError;
self.state = NoiseProtocolState::Failed(err.to_string());
Some(Err(err))
}
} else {
// Handshake continues
Some(Ok(buffer[..len].to_vec()))
}
}
Err(e) => {
let err = NoiseError::from(e);
self.state = NoiseProtocolState::Failed(err.to_string());
Some(Err(err))
}
}
} else {
// Not our turn
None
}
}
NoiseProtocolState::Transport(_) | NoiseProtocolState::Failed(_) => {
// No handshake messages to send in these states
None
}
}
}
/// Encrypts an application data payload for sending during the Transport phase.
///
/// Returns the ciphertext (payload + 16-byte tag).
/// Errors if not in Transport state or encryption fails.
pub fn write_message(&mut self, payload: &[u8]) -> Result<Vec<u8>, NoiseError> {
match &mut self.state {
NoiseProtocolState::Transport(transport_state) => {
let mut buffer = vec![0u8; payload.len() + 16]; // Payload + tag
match transport_state.write_message(payload, &mut buffer) {
Ok(len) => Ok(buffer[..len].to_vec()),
Err(e) => {
let err = NoiseError::from(e);
self.state = NoiseProtocolState::Failed(err.to_string());
Err(err)
}
}
}
NoiseProtocolState::Handshaking(_) | NoiseProtocolState::Failed(_) => {
Err(NoiseError::IncorrectStateError)
}
}
}
/// Returns true if the protocol is in the transport phase (handshake complete).
pub fn is_transport(&self) -> bool {
matches!(self.state, NoiseProtocolState::Transport(_))
}
/// Returns true if the protocol has failed.
pub fn is_failed(&self) -> bool {
matches!(self.state, NoiseProtocolState::Failed(_))
}
/// Check if the handshake has finished and the protocol is in transport mode.
pub fn is_handshake_finished(&self) -> bool {
matches!(self.state, NoiseProtocolState::Transport(_))
}
/// Inject a PSK into the Noise HandshakeState.
///
/// This allows dynamic PSK injection after HandshakeState construction,
/// which is required for PSQ (Post-Quantum Secure PSK) integration where
/// the PSK is derived during the handshake process.
///
/// # Arguments
/// * `index` - PSK index (typically 3 for XKpsk3 pattern)
/// * `psk` - The pre-shared key bytes to inject
///
/// # Errors
/// Returns an error if:
/// - Not in handshake state
/// - The underlying snow library rejects the PSK
pub fn set_psk(&mut self, index: u8, psk: &[u8]) -> Result<(), NoiseError> {
match &mut self.state {
NoiseProtocolState::Handshaking(handshake_state) => {
handshake_state
.set_psk(index as usize, psk)
.map_err(NoiseError::ProtocolError)?;
Ok(())
}
_ => Err(NoiseError::IncorrectStateError),
}
}
}
+92 -107
View File
@@ -6,10 +6,7 @@ use crate::message::{LpMessage, MessageType};
use crate::replay::ReceivingKeyCounterValidator;
use bytes::{BufMut, BytesMut};
use nym_lp_common::format_debug_bytes;
use parking_lot::Mutex;
use std::fmt::Write;
use std::fmt::{Debug, Formatter};
use std::sync::Arc;
use tracing::warn;
#[allow(dead_code)]
@@ -31,11 +28,38 @@ pub mod version {
pub const CURRENT: u8 = 1;
}
#[derive(Clone)]
pub struct EncryptedLpPacket {
// The outer header that's sent in plaintext
pub(crate) outer_header: OuterHeader,
// The ciphertext containing the inner header and the payload
pub(crate) ciphertext: Vec<u8>,
}
impl Debug for EncryptedLpPacket {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", format_debug_bytes(&self.debug_bytes())?)
}
}
impl EncryptedLpPacket {
pub(crate) fn debug_bytes(&self) -> Vec<u8> {
let mut bytes = BytesMut::new();
self.encode(&mut bytes);
bytes.freeze().to_vec()
}
pub fn encode(&self, dst: &mut BytesMut) {
self.outer_header.encode(dst);
dst.put_slice(&self.ciphertext)
}
}
#[derive(Clone)]
pub struct LpPacket {
pub(crate) header: LpHeader,
pub(crate) message: LpMessage,
pub(crate) trailer: [u8; TRAILER_LEN],
}
impl Debug for LpPacket {
@@ -46,43 +70,13 @@ impl Debug for LpPacket {
impl LpPacket {
pub fn new(header: LpHeader, message: LpMessage) -> Self {
Self {
header,
message,
trailer: [0; TRAILER_LEN],
}
Self { header, message }
}
pub fn typ(&self) -> MessageType {
self.message.typ()
}
/// Compute a hash of the message payload
///
/// This can be used for message integrity verification or deduplication
pub fn hash_payload(&self) -> [u8; 32] {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
let mut buffer = BytesMut::new();
// Include message type and content in the hash
buffer.put_slice(&(self.message.typ() as u16).to_le_bytes());
self.message.encode_content(&mut buffer);
hasher.update(&buffer);
hasher.finalize().into()
}
pub fn hash_payload_hex(&self) -> String {
let hash = self.hash_payload();
hash.iter()
.fold(String::with_capacity(hash.len() * 2), |mut acc, byte| {
let _ = write!(acc, "{:02x}", byte);
acc
})
}
pub fn message(&self) -> &LpMessage {
&self.message
}
@@ -93,17 +87,15 @@ impl LpPacket {
pub(crate) fn debug_bytes(&self) -> Vec<u8> {
let mut bytes = BytesMut::new();
self.encode(&mut bytes);
self.dbg_encode(&mut bytes);
bytes.freeze().to_vec()
}
pub(crate) fn encode(&self, dst: &mut BytesMut) {
self.header.encode(dst);
pub(crate) fn dbg_encode(&self, dst: &mut BytesMut) {
self.header.dbg_encode(dst);
dst.put_slice(&(self.message.typ() as u16).to_le_bytes());
self.message.encode_content(dst);
dst.put_slice(&self.trailer)
}
/// Validate packet counter against a replay protection validator
@@ -112,10 +104,9 @@ impl LpPacket {
/// any expensive processing is done.
pub fn validate_counter(
&self,
validator: &Arc<Mutex<ReceivingKeyCounterValidator>>,
validator: &ReceivingKeyCounterValidator,
) -> Result<(), LpError> {
let guard = validator.lock();
guard.will_accept_branchless(self.header.counter)?;
validator.will_accept_branchless(self.header.outer.counter)?;
Ok(())
}
@@ -124,22 +115,13 @@ impl LpPacket {
/// This should be called after a packet has been successfully processed.
pub fn mark_received(
&self,
validator: &Arc<Mutex<ReceivingKeyCounterValidator>>,
validator: &mut ReceivingKeyCounterValidator,
) -> Result<(), LpError> {
let mut guard = validator.lock();
guard.mark_did_receive_branchless(self.header.counter)?;
validator.mark_did_receive_branchless(self.header.outer.counter)?;
Ok(())
}
}
/// Session ID used for ClientHello bootstrap packets before session is established.
///
/// When a client first connects, it sends a ClientHello packet with receiver_idx=0
/// because neither side can compute the deterministic session ID yet (requires
/// both parties' X25519 keys). After ClientHello is processed, both sides derive
/// the same session ID from their keys, and all subsequent packets use that ID.
pub const BOOTSTRAP_RECEIVER_IDX: u32 = 0;
/// Outer header (12 bytes) - always cleartext, used for routing.
///
/// This is the first 12 bytes of every LP packet, containing only the fields
@@ -171,66 +153,36 @@ impl OuterHeader {
})
}
pub fn encode(&self) -> [u8; Self::SIZE] {
let mut buf = [0u8; Self::SIZE];
buf[0..4].copy_from_slice(&self.receiver_idx.to_le_bytes());
buf[4..12].copy_from_slice(&self.counter.to_le_bytes());
buf
}
/// Encode directly into a BytesMut buffer
pub fn encode_into(&self, dst: &mut BytesMut) {
pub fn encode(&self, dst: &mut BytesMut) {
dst.put_slice(&self.receiver_idx.to_le_bytes());
dst.put_slice(&self.counter.to_le_bytes());
}
}
/// Internal LP header representation containing all logical header fields.
///
/// **Note**: This struct represents the LOGICAL header, not the wire format.
/// On the wire, packets use the unified format where:
/// - `OuterHeader` (receiver_idx + counter) always comes first (12 bytes, cleartext)
/// - Inner content (version + reserved + payload) follows (cleartext or encrypted)
///
/// The `LpHeader::encode()` method outputs the old logical format for debug purposes only.
/// Use `serialize_lp_packet()` in codec.rs for actual wire serialization.
#[derive(Debug, Clone)]
pub struct LpHeader {
/// InnerHeader header (8 bytes) - encrypted, used for message parsing
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InnerHeader {
pub protocol_version: u8,
pub reserved: [u8; 3],
pub receiver_idx: u32,
pub counter: u64,
pub message_type: MessageType,
}
impl LpHeader {
pub const SIZE: usize = 16;
}
impl InnerHeader {
pub const SIZE: usize = 8; // protocol_version(1) + reserved(3) + message_type(4)
impl LpHeader {
pub fn new(receiver_idx: u32, counter: u64, protocol_version: u8) -> Self {
Self {
protocol_version,
reserved: [0u8; 3],
receiver_idx,
counter,
}
}
pub fn encode(&self, dst: &mut BytesMut) {
pub(crate) fn encode(&self, dst: &mut BytesMut) {
// protocol version
dst.put_u8(self.protocol_version);
// reserved
dst.put_slice(&self.reserved);
// sender index
dst.put_slice(&self.receiver_idx.to_le_bytes());
// counter
dst.put_slice(&self.counter.to_le_bytes());
// message type
dst.put_slice(&(self.message_type as u32).to_le_bytes());
}
pub fn parse(src: &[u8]) -> Result<Self, LpError> {
pub(crate) fn parse(src: &[u8]) -> Result<Self, LpError> {
if src.len() < Self::SIZE {
return Err(LpError::InsufficientBufferSize);
}
@@ -259,30 +211,63 @@ impl LpHeader {
warn!("received non-zero reserved bytes. got: {reserved:?}");
}
let mut receiver_idx_bytes = [0u8; 4];
receiver_idx_bytes.copy_from_slice(&src[4..8]);
let receiver_idx = u32::from_le_bytes(receiver_idx_bytes);
let msg_type_raw = u32::from_le_bytes([src[4], src[5], src[6], src[7]]);
let message_type = MessageType::from_u32(msg_type_raw)
.ok_or_else(|| LpError::invalid_message_type(msg_type_raw))?;
let mut counter_bytes = [0u8; 8];
counter_bytes.copy_from_slice(&src[8..16]);
let counter = u64::from_le_bytes(counter_bytes);
Ok(LpHeader {
Ok(InnerHeader {
protocol_version,
reserved,
receiver_idx,
counter,
message_type,
})
}
}
/// Internal LP header representation containing all logical header fields.
///
/// **Note**: This struct represents the LOGICAL header, not the wire format.
/// On the wire, packets use the unified format where:
/// - `OuterHeader` (receiver_idx + counter) always comes first (12 bytes, cleartext)
/// - Inner content (version + reserved + payload) follows (cleartext or encrypted)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LpHeader {
pub outer: OuterHeader,
pub inner: InnerHeader,
}
impl LpHeader {
pub fn new(
receiver_idx: u32,
counter: u64,
protocol_version: u8,
message_type: MessageType,
) -> Self {
Self {
outer: OuterHeader {
receiver_idx,
counter,
},
inner: InnerHeader {
protocol_version,
reserved: [0u8; 3],
message_type,
},
}
}
pub(crate) fn dbg_encode(&self, dst: &mut BytesMut) {
self.outer.encode(dst);
self.inner.encode(dst);
}
/// Get the counter value from the header
pub fn counter(&self) -> u64 {
self.counter
self.outer.counter
}
/// Get the sender index from the header
pub fn receiver_idx(&self) -> u32 {
self.receiver_idx
self.outer.receiver_idx
}
}
+4 -24
View File
@@ -1,12 +1,9 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{ClientHelloData, LpError};
use crate::LpError;
use libcrux_psq::handshake::types::{DHKeyPair, DHPublicKey};
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_kkt::key_utils::{
generate_keypair_mceliece, generate_keypair_mlkem, generate_keypair_x25519,
};
use nym_kkt::keys::KEMKeys;
use nym_kkt_ciphersuite::{Ciphersuite, KEM, KEMKeyDigests, SignatureScheme, SigningKeyDigests};
use std::collections::HashMap;
@@ -43,15 +40,6 @@ impl LpLocalPeer {
}
}
pub fn build_client_hello_data(&self, timestamp: u64) -> ClientHelloData {
todo!()
// ClientHelloData::new_with_fresh_salt(
// *self.x25519().public_key(),
// *self.ed25519().public_key(),
// timestamp,
// )
}
#[must_use]
pub fn with_kem_keys(mut self, kem_keys: Arc<KEMKeys>) -> Self {
self.kem_keypairs = Some(kem_keys);
@@ -66,14 +54,6 @@ impl LpLocalPeer {
&self.x25519
}
// /// Returns the reference to the KEM Public key of the peer (if available).
// pub fn get_kem_key_handle(&self) -> Result<&x25519::PublicKey, LpError> {
// self.kem_psq
// .as_ref()
// .map(|kp| kp.public_key())
// .ok_or(LpError::ResponderWithMissingKEMKey)
// }
/// Convert this `LpLocalPeer` into a valid `LpRemotePeer` that can be used within tests
#[doc(hidden)]
pub fn as_remote(&self) -> LpRemotePeer {
@@ -202,15 +182,15 @@ pub fn random_peer<'a, R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> LpLoc
let mut rng09 = nym_test_utils::helpers::seeded_rng_09(seed);
let x25519 = Arc::new(generate_keypair_x25519(&mut rng09));
let x25519 = Arc::new(nym_kkt::key_utils::generate_keypair_x25519(&mut rng09));
LpLocalPeer {
ciphersuite: Ciphersuite::default(),
ed25519,
x25519,
kem_keypairs: Some(Arc::new(KEMKeys::new(
generate_keypair_mceliece(&mut rng09),
generate_keypair_mlkem(&mut rng09),
nym_kkt::key_utils::generate_keypair_mceliece(&mut rng09),
nym_kkt::key_utils::generate_keypair_mlkem(&mut rng09),
))),
}
}
-788
View File
@@ -1,788 +0,0 @@
// 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.
//!
//! PSQ is embedded in Noise (not separate protocol) because:
//! 1. Single round-trip: PSQ ciphertext piggybacks on Noise handshake messages
//! 2. PSK binding: Noise XKpsk3 pattern authenticates both ECDH and PSQ-derived PSK
//! 3. Simpler state machine: No separate PSQ negotiation phase needed
//! 4. Atomic security: Session establishment either succeeds fully or fails completely
//!
//! Two approaches are supported:
//! - **Legacy ECDH-only** (`derive_psk`) - Simple but no post-quantum security
//! - **PSQ-enhanced** (`derive_psk_with_psq_*`) - Combines ECDH with post-quantum KEM
//!
//! ## Error Handling Strategy
//!
//! **PSQ failures always abort the handshake cleanly with no retry or fallback.**
//!
//! ### Rationale
//!
//! PSQ errors indicate:
//! - **Authentication failures** (CredError) - Potential attack or misconfiguration
//! - **Timing failures** (TimestampElapsed) - Replay attacks or clock skew
//! - **Crypto failures** (CryptoError) - Library bugs or hardware faults
//! - **Serialization failures** (Serialization) - Protocol violations or corruption
//!
//! None of these are transient errors that benefit from retry. Falling back to
//! ECDH-only PSK would silently degrade post-quantum security.
//!
//! ### Error Recovery Behavior
//!
//! On any PSQ error:
//! 1. Function returns `Err(LpError)` immediately
//! 2. Session state remains unchanged (dummy PSK, clean Noise state)
//! 3. Handshake aborts - caller must start fresh connection
//! 4. Error is logged with diagnostic context
//!
//! ### State Guarantees on Error
//!
//! - **`psq_state`**: Remains in `NotStarted` (initiator) or `ResponderWaiting` (responder)
//! - **Noise `HandshakeState`**: PSK slot 3 = dummy `[0u8; 32]` (not modified on error)
//! - **No partial data**: All allocations are stack-local to failed function
//! - **No cleanup needed**: No state was mutated
use crate::LpError;
use libcrux_psq::handshake::types::{DHPrivateKey, DHPublicKey};
use libcrux_psq::v1::cred::{Authenticator, Ed25519};
use libcrux_psq::v1::impls::X25519 as PsqX25519;
use libcrux_psq::v1::psk_registration::{Initiator, InitiatorMsg, Responder};
use libcrux_psq::v1::traits::{Ciphertext as PsqCiphertext, PSQ};
use nym_crypto::asymmetric::ed25519;
use std::time::Duration;
use tls_codec::{Deserialize as TlsDeserializeTrait, Serialize as TlsSerializeTrait};
/// Context string for Blake3 KDF domain separation (PSQ-enhanced).
const PSK_PSQ_CONTEXT: &str = "nym-lp-psk-psq-v1";
/// Session context for PSQ protocol.
const PSQ_SESSION_CONTEXT: &[u8] = b"nym-lp-psq-session";
/// Context string for subsession PSK derivation.
const SUBSESSION_PSK_CONTEXT: &str = "lp-subsession-psk-v1";
/// Result from PSQ initiator message creation.
///
/// Contains all outputs needed for session establishment:
/// - `psk`: Final derived PSK for Noise handshake (ECDH || K_pq || salt → Blake3)
/// - `payload`: Serialized PSQ message to send to responder
/// - `pq_shared_secret`: Raw K_pq from KEM encapsulation (for subsession derivation)
#[derive(Debug)]
pub struct PsqInitiatorResult {
/// Final PSK for Noise XKpsk3 handshake
pub psk: [u8; 32],
/// Serialized PSQ payload to embed in handshake message
pub payload: Vec<u8>,
/// Raw PQ shared secret (K_pq) before KDF combination.
/// Used for deriving subsession PSKs to preserve PQ protection.
pub pq_shared_secret: [u8; 32],
}
/// Result from PSQ responder message processing.
///
/// Contains all outputs needed for session establishment:
/// - `psk`: Final derived PSK for Noise handshake (matches initiator's)
/// - `psk_handle`: Encrypted PSK handle (ctxt_B) to send back to initiator
/// - `pq_shared_secret`: Raw K_pq from KEM decapsulation (for subsession derivation)
#[derive(Debug)]
pub struct PsqResponderResult {
/// Final PSK for Noise XKpsk3 handshake
pub psk: [u8; 32],
/// Encrypted PSK handle (ctxt_B) from PSQ responder message
pub psk_handle: Vec<u8>,
/// Raw PQ shared secret (K_pq) before KDF combination.
/// Used for deriving subsession PSKs to preserve PQ protection.
pub pq_shared_secret: [u8; 32],
}
/// Derives a PSK using PSQ (Post-Quantum Secure PSK) protocol - Initiator side.
///
/// This function combines classical ECDH with post-quantum KEM to provide forward secrecy
/// and HNDL (Harvest-Now, Decrypt-Later) resistance.
///
/// # Formula
/// ```text
/// ecdh_secret = ECDH(local_x25519_private, remote_x25519_public)
/// (psq_psk, ct) = PSQ_Encapsulate(remote_kem_public, session_context)
/// psk = Blake3_derive_key(
/// context="nym-lp-psk-psq-v1",
/// input=ecdh_secret || psq_psk || salt
/// )
/// ```
///
/// # Arguments
/// * `local_x25519_private` - Initiator's X25519 private key (for Noise)
/// * `remote_x25519_public` - Responder's X25519 public key (for Noise)
/// * `remote_kem_public` - Responder's KEM public key (obtained via KKT)
/// * `salt` - 32-byte salt for session binding
///
/// # Returns
/// * `Ok((psk, ciphertext))` - PSK and ciphertext to send to responder
/// * `Err(LpError)` - If PSQ encapsulation fails
///
/// # Example
/// ```ignore
/// // Client side (after KKT exchange)
/// let (psk, ciphertext) = derive_psk_with_psq_initiator(
/// client_x25519_private,
/// gateway_x25519_public,
/// &gateway_kem_key, // from KKT
/// &salt
/// )?;
/// // Send ciphertext to gateway
/// ```
pub fn derive_psk_with_psq_initiator(
local_x25519_private: &DHPrivateKey,
remote_x25519_public: &DHPublicKey,
// remote_kem_public: &EncapsulationKey,
remote_kem_public: (),
salt: &[u8; 32],
) -> Result<([u8; 32], Vec<u8>), LpError> {
// Step 1: Classical ECDH for baseline security
// let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public);
let ecdh_secret: [u8; 32] = unimplemented!("unexposed by libcrux");
todo!()
//
// // Step 2: PSQ encapsulation for post-quantum security
// // KEM algorithm migration path:
// // - X25519: Current default for testing/compatibility (no HNDL resistance)
// // - MlKem768: Future production default (NIST PQ Level 3, HNDL resistant)
// // - XWing: Maximum security option (hybrid X25519 + ML-KEM)
// // Migration: Update LpConfig.kem_algorithm, no protocol changes needed.
// // KKT protocol adapts automatically to different KEM key sizes.
// let kem_pk = match remote_kem_public {
// EncapsulationKey::X25519(pk) => pk,
// _ => {
// return Err(LpError::KKTError(
// "Only X25519 KEM is currently supported for PSQ".to_string(),
// ));
// }
// };
//
// let mut rng = rand09::rng();
// let (psq_psk, ciphertext) =
// PsqX25519::encapsulate_psq(kem_pk, PSQ_SESSION_CONTEXT, &mut rng)
// .map_err(|e| LpError::Internal(format!("PSQ encapsulation failed: {:?}", e)))?;
//
// // Step 3: Combine ECDH + PSQ via Blake3 KDF
// let mut combined = Vec::with_capacity(64 + psq_psk.len());
// combined.extend_from_slice(&ecdh_secret);
// combined.extend_from_slice(&psq_psk); // psq_psk is [u8; 32], need &
// combined.extend_from_slice(salt);
//
// let final_psk = nym_crypto::hkdf::blake3::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
//
// // Serialize ciphertext using TLS encoding for transport
// let ct_bytes = ciphertext
// .tls_serialize_detached()
// .map_err(|e| LpError::Internal(format!("Ciphertext serialization failed: {:?}", e)))?;
//
// Ok((final_psk, ct_bytes))
}
/// Derives a PSK using PSQ (Post-Quantum Secure PSK) protocol - Responder side.
///
/// This function decapsulates the ciphertext from the initiator and combines it with
/// ECDH to derive the same PSK.
///
/// # Formula
/// ```text
/// ecdh_secret = ECDH(local_x25519_private, remote_x25519_public)
/// psq_psk = PSQ_Decapsulate(local_kem_keypair, ciphertext, session_context)
/// psk = Blake3_derive_key(
/// context="nym-lp-psk-psq-v1",
/// input=ecdh_secret || psq_psk || salt
/// )
/// ```
///
/// # Arguments
/// * `local_x25519_private` - Responder's X25519 private key (for Noise)
/// * `remote_x25519_public` - Initiator's X25519 public key (for Noise)
/// * `local_kem_keypair` - Responder's KEM keypair (decapsulation key, public key)
/// * `ciphertext` - PSQ ciphertext from initiator
/// * `salt` - 32-byte salt for session binding
///
/// # Returns
/// * `Ok(psk)` - Derived PSK
/// * `Err(LpError)` - If PSQ decapsulation fails
///
/// # Example
/// ```ignore
/// // Gateway side (after receiving ciphertext)
/// let psk = derive_psk_with_psq_responder(
/// gateway_x25519_private,
/// client_x25519_public,
/// (&gateway_kem_sk, &gateway_kem_pk),
/// &ciphertext, // from client
/// &salt
/// )?;
/// ```
pub fn derive_psk_with_psq_responder(
local_x25519_private: &DHPrivateKey,
remote_x25519_public: &DHPublicKey,
// local_kem_keypair: (&DecapsulationKey, &EncapsulationKey),
local_kem_keypair: ((), ()),
ciphertext: &[u8],
salt: &[u8; 32],
) -> Result<[u8; 32], LpError> {
// Step 1: Classical ECDH for baseline security
// let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public);
todo!()
// let ecdh_secret: [u8; 32] = unimplemented!("unexposed by libcrux");
//
// // Step 2: Extract X25519 keypair from DecapsulationKey/EncapsulationKey
// let (kem_sk, kem_pk) = match (local_kem_keypair.0, local_kem_keypair.1) {
// (DecapsulationKey::X25519(sk), EncapsulationKey::X25519(pk)) => (sk, pk),
// _ => {
// return Err(LpError::KKTError(
// "Only X25519 KEM is currently supported for PSQ".to_string(),
// ));
// }
// };
//
// // Step 3: Deserialize ciphertext using TLS decoding
// let ct = PsqCiphertext::<PsqX25519>::tls_deserialize(&mut &ciphertext[..])
// .map_err(|e| LpError::Internal(format!("Ciphertext deserialization failed: {:?}", e)))?;
//
// // Step 4: PSQ decapsulation for post-quantum security
// let psq_psk = PsqX25519::decapsulate_psq(kem_sk, kem_pk, &ct, PSQ_SESSION_CONTEXT)
// .map_err(|e| LpError::Internal(format!("PSQ decapsulation failed: {:?}", e)))?;
//
// // Step 5: Combine ECDH + PSQ via Blake3 KDF (same formula as initiator)
// let mut combined = Vec::with_capacity(64 + psq_psk.len());
// combined.extend_from_slice(&ecdh_secret);
// combined.extend_from_slice(&psq_psk); // psq_psk is [u8; 32], need &
// combined.extend_from_slice(salt);
//
// let final_psk = nym_crypto::hkdf::blake3::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
//
// Ok(final_psk)
}
/// PSQ protocol wrapper for initiator (client) side.
///
/// Creates a PSQ initiator message with Ed25519 authentication, following the protocol:
/// 1. Encapsulate PSK using responder's KEM key
/// 2. Derive PSK and AEAD keys from K_pq
/// 3. Sign the encapsulation with Ed25519
/// 4. AEAD encrypt (timestamp || signature || public_key)
///
/// Returns (PSK, serialized_payload) where payload includes enc_pq and encrypted auth data.
///
/// # Arguments
/// * `local_x25519_private` - Client's X25519 private key (for hybrid ECDH)
/// * `remote_x25519_public` - Gateway's X25519 public key (for hybrid ECDH)
/// * `remote_kem_public` - Gateway's PQ KEM public key (from KKT)
/// * `client_ed25519_sk` - Client's Ed25519 signing key
/// * `client_ed25519_pk` - Client's Ed25519 public key (credential)
/// * `salt` - Session salt
/// * `session_context` - Context bytes for PSQ (e.g., b"nym-lp-psq-session")
///
/// # Returns
/// `PsqInitiatorResult` containing PSK, payload, and raw PQ shared secret
pub fn psq_initiator_create_message(
local_x25519_private: &DHPrivateKey,
remote_x25519_public: &DHPublicKey,
// remote_kem_public: &EncapsulationKey,
remote_kem_public: (),
client_ed25519_sk: &ed25519::PrivateKey,
client_ed25519_pk: &ed25519::PublicKey,
salt: &[u8; 32],
session_context: &[u8],
) -> Result<PsqInitiatorResult, LpError> {
// Step 1: Classical ECDH for baseline security
// let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public);
let ecdh_secret: [u8; 32] = unimplemented!("unexposed by libcrux");
todo!()
//
// // Step 2: PSQ v1 with Ed25519 authentication
// // Extract X25519 KEM key from EncapsulationKey
// let kem_pk = match remote_kem_public {
// EncapsulationKey::X25519(pk) => pk,
// _ => {
// return Err(LpError::KKTError(
// "Only X25519 KEM is currently supported for PSQ".to_string(),
// ));
// }
// };
//
// // Convert nym Ed25519 keys to libcrux format
// type Ed25519VerificationKey = <Ed25519 as Authenticator>::VerificationKey;
// let ed25519_sk_bytes = client_ed25519_sk.to_bytes();
// let ed25519_pk_bytes = client_ed25519_pk.to_bytes();
// let ed25519_verification_key = Ed25519VerificationKey::from_bytes(ed25519_pk_bytes);
//
// // Use PSQ v1 API with Ed25519 authentication
// let mut rng = rand09::rng();
// let (state, initiator_msg) = Initiator::send_initial_message::<Ed25519, PsqX25519>(
// session_context,
// Duration::from_secs(3600), // 1 hour expiry
// kem_pk,
// &ed25519_sk_bytes,
// &ed25519_verification_key,
// &mut rng,
// )
// .map_err(|e| {
// tracing::error!(
// "PSQ initiator failed - KEM encapsulation or signing error: {:?}",
// e
// );
// LpError::Internal(format!("PSQ v1 send_initial_message failed: {:?}", e))
// })?;
//
// // Extract PSQ shared secret (unregistered PSK) - this is K_pq
// let psq_psk = state.unregistered_psk();
//
// // pq_shared_secret is the raw K_pq from KEM encapsulation.
// // Store it for subsession derivation before it's combined with ECDH.
// let pq_shared_secret: [u8; 32] = *psq_psk;
//
// // Step 3: Combine ECDH + PSQ via Blake3 KDF
// let mut combined = Vec::with_capacity(64 + psq_psk.len());
// combined.extend_from_slice(&ecdh_secret);
// combined.extend_from_slice(psq_psk); // psq_psk is already a &[u8; 32]
// combined.extend_from_slice(salt);
//
// let final_psk = nym_crypto::hkdf::blake3::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
//
// // Serialize InitiatorMsg with TLS encoding for transport
// let msg_bytes = initiator_msg
// .tls_serialize_detached()
// .map_err(|e| LpError::Internal(format!("InitiatorMsg serialization failed: {:?}", e)))?;
//
// Ok(PsqInitiatorResult {
// psk: final_psk,
// payload: msg_bytes,
// pq_shared_secret,
// })
}
/// PSQ protocol wrapper for responder (gateway) side.
///
/// Processes a PSQ initiator message, verifies authentication, and derives PSK.
/// Follows the protocol:
/// 1. Decapsulate to get K_pq
/// 2. Derive AEAD keys and verify encrypted auth data
/// 3. Verify Ed25519 signature
/// 4. Check timestamp validity
/// 5. Derive PSK
///
/// # Arguments
/// * `local_x25519_private` - Gateway's X25519 private key (for hybrid ECDH)
/// * `remote_x25519_public` - Client's X25519 public key (for hybrid ECDH)
/// * `local_kem_keypair` - Gateway's PQ KEM keypair
/// * `initiator_ed25519_pk` - Client's Ed25519 public key (for signature verification)
/// * `psq_payload` - Serialized PSQ payload from initiator
/// * `salt` - Session salt (must match initiator's)
/// * `session_context` - Context bytes for PSQ
///
/// # Returns
/// `PsqResponderResult` containing PSK, PSK handle, and raw PQ shared secret
pub fn psq_responder_process_message(
local_x25519_private: &DHPrivateKey,
remote_x25519_public: &DHPublicKey,
// local_kem_keypair: (&DecapsulationKey, &EncapsulationKey),
local_kem_keypair: ((), ()),
initiator_ed25519_pk: &ed25519::PublicKey,
psq_payload: &[u8],
salt: &[u8; 32],
session_context: &[u8],
) -> Result<PsqResponderResult, LpError> {
// Step 1: Classical ECDH for baseline security
// let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public);
let ecdh_secret: [u8; 32] = unimplemented!("unexposed by libcrux");
todo!()
//
// // Step 2: Extract X25519 keypair from DecapsulationKey/EncapsulationKey
// let (kem_sk, kem_pk) = match (local_kem_keypair.0, local_kem_keypair.1) {
// (DecapsulationKey::X25519(sk), EncapsulationKey::X25519(pk)) => (sk, pk),
// _ => {
// return Err(LpError::KKTError(
// "Only X25519 KEM is currently supported for PSQ".to_string(),
// ));
// }
// };
//
// // Step 3: Deserialize InitiatorMsg using TLS decoding
// let initiator_msg = InitiatorMsg::<PsqX25519>::tls_deserialize(&mut &psq_payload[..])
// .map_err(|e| LpError::Internal(format!("InitiatorMsg deserialization failed: {:?}", e)))?;
//
// // Step 4: Convert nym Ed25519 public key to libcrux VerificationKey format
// type Ed25519VerificationKey = <Ed25519 as Authenticator>::VerificationKey;
// let initiator_ed25519_pk_bytes = initiator_ed25519_pk.to_bytes();
// let initiator_verification_key = Ed25519VerificationKey::from_bytes(initiator_ed25519_pk_bytes);
//
// // Step 5: PSQ v1 responder processing with Ed25519 verification
// let (registered_psk, responder_msg) = Responder::send::<Ed25519, PsqX25519>(
// b"nym-lp-handle", // PSK storage handle
// Duration::from_secs(3600), // 1 hour expiry (must match initiator)
// session_context, // Must match initiator's session_context
// kem_pk, // Responder's public key
// kem_sk, // Responder's secret key
// &initiator_verification_key, // Initiator's Ed25519 public key for verification
// &initiator_msg, // InitiatorMsg to verify and process
// )
// .map_err(|e| {
// use libcrux_psq::v1::Error as PsqError;
// match e {
// PsqError::CredError => {
// tracing::warn!(
// "PSQ responder auth failure - invalid Ed25519 signature (potential attack)"
// );
// }
// PsqError::TimestampElapsed | PsqError::RegistrationError => {
// tracing::warn!(
// "PSQ responder timing failure - TTL expired (potential replay attack)"
// );
// }
// _ => {
// tracing::error!("PSQ responder failed - {:?}", e);
// }
// }
// LpError::Internal(format!("PSQ v1 responder send failed: {:?}", e))
// })?;
//
// // Extract the PSQ PSK from the registered PSK - this is K_pq
// let psq_psk = registered_psk.psk;
//
// // pq_shared_secret is the raw K_pq from KEM decapsulation.
// // Store it for subsession derivation before it's combined with ECDH.
// let pq_shared_secret: [u8; 32] = psq_psk;
//
// // Step 6: Combine ECDH + PSQ via Blake3 KDF (same formula as initiator)
// let mut combined = Vec::with_capacity(64 + psq_psk.len());
// combined.extend_from_slice(&ecdh_secret);
// combined.extend_from_slice(&psq_psk); // psq_psk is [u8; 32], need &
// combined.extend_from_slice(salt);
//
// let final_psk = nym_crypto::hkdf::blake3::derive_key_blake3(PSK_PSQ_CONTEXT, &combined, &[]);
//
// // Step 7: Serialize ResponderMsg (contains ctxt_B - encrypted PSK handle)
// use tls_codec::Serialize;
// let responder_msg_bytes = responder_msg
// .tls_serialize_detached()
// .map_err(|e| LpError::Internal(format!("ResponderMsg serialization failed: {:?}", e)))?;
//
// Ok(PsqResponderResult {
// psk: final_psk,
// psk_handle: responder_msg_bytes,
// pq_shared_secret,
// })
}
/// Derive subsession PSK from parent's PQ shared secret.
///
/// Uses Blake3 KDF with domain separation to derive unique PSK for each subsession.
/// This preserves PQ protection: subsession keys inherit quantum resistance from
/// parent's KEM shared secret (K_pq).
///
/// # Security Model
///
/// Subsessions use Noise KKpsk0 pattern where:
/// - Both parties already know each other's static X25519 keys (from parent session)
/// - PSK provides PQ protection by deriving from parent's K_pq
/// - Each subsession gets unique PSK via index parameter (prevents key reuse)
///
/// # Arguments
/// * `pq_shared_secret` - Parent session's K_pq (32 bytes from KEM)
/// * `subsession_index` - Monotonic index for this subsession (prevents reuse)
///
/// # Returns
/// 32-byte PSK for Noise KKpsk0 handshake
pub fn derive_subsession_psk(pq_shared_secret: &[u8; 32], subsession_index: u64) -> [u8; 32] {
nym_crypto::hkdf::blake3::derive_key_blake3(
SUBSESSION_PSK_CONTEXT,
pq_shared_secret,
&subsession_index.to_le_bytes(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use libcrux_psq::handshake::types::DHKeyPair;
fn generate_x25519_keypair() -> DHKeyPair {
DHKeyPair::new(&mut rand09::rng())
}
#[test]
fn test_psk_derivation_is_symmetric() {
todo!()
// let keypair_1 = generate_x25519_keypair();
// let keypair_2 = generate_x25519_keypair();
// let salt = [2u8; 32];
//
// let mut rng = &mut rand09::rng();
// let (_kem_sk, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
// let enc_key = EncapsulationKey::X25519(kem_pk);
// let dec_key = DecapsulationKey::X25519(_kem_sk);
//
// // Client derives PSK
// let (client_psk, ciphertext) =
// derive_psk_with_psq_initiator(keypair_1.sk(), &keypair_2.pk, &enc_key, &salt).unwrap();
//
// // Gateway derives PSK from their perspective
// let gateway_psk = derive_psk_with_psq_responder(
// keypair_2.sk(),
// &keypair_1.pk,
// (&dec_key, &enc_key),
// &ciphertext,
// &salt,
// )
// .unwrap();
//
// assert_eq!(
// client_psk, gateway_psk,
// "Both sides should derive identical PSK"
// );
}
#[test]
fn test_different_salts_produce_different_psks() {
todo!()
// let keypair_1 = generate_x25519_keypair();
// let keypair_2 = generate_x25519_keypair();
//
// let salt1 = [1u8; 32];
// let salt2 = [2u8; 32];
// let mut rng = &mut rand09::rng();
// let (_kem_sk, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
// let enc_key = EncapsulationKey::X25519(kem_pk);
//
// let psk1 =
// derive_psk_with_psq_initiator(keypair_1.sk(), &keypair_2.pk, &enc_key, &salt1).unwrap();
// let psk2 =
// derive_psk_with_psq_initiator(keypair_1.sk(), &keypair_2.pk, &enc_key, &salt2).unwrap();
//
// assert_ne!(psk1, psk2, "Different salts should produce different PSKs");
}
#[test]
fn test_different_keys_produce_different_psks() {
todo!()
// let keypair_1 = generate_x25519_keypair();
// let keypair_2 = generate_x25519_keypair();
// let keypair_3 = generate_x25519_keypair();
// let salt = [3u8; 32];
//
// let mut rng = &mut rand09::rng();
// let (_kem_sk, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
// let enc_key = EncapsulationKey::X25519(kem_pk);
//
// let psk1 =
// derive_psk_with_psq_initiator(keypair_1.sk(), &keypair_2.pk, &enc_key, &salt).unwrap();
// let psk2 =
// derive_psk_with_psq_initiator(keypair_1.sk(), &keypair_3.pk, &enc_key, &salt).unwrap();
//
// assert_ne!(
// psk1, psk2,
// "Different remote keys should produce different PSKs"
// );
}
#[test]
fn test_psq_derivation_deterministic() {
todo!()
// let mut rng = rand09::rng();
//
// // Generate X25519 keypairs for Noise
// let client_keypair = generate_x25519_keypair();
// let gateway_keypair = generate_x25519_keypair();
//
// // Generate KEM keypair for PSQ
// let (kem_sk, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
// let enc_key = EncapsulationKey::X25519(kem_pk);
// let dec_key = DecapsulationKey::X25519(kem_sk);
//
// let salt = [1u8; 32];
//
// // Derive PSK twice with same inputs (initiator side)
// let (_psk1, ct1) = derive_psk_with_psq_initiator(
// client_keypair.sk(),
// &gateway_keypair.pk,
// &enc_key,
// &salt,
// )
// .unwrap();
//
// let (_psk2, _ct2) = derive_psk_with_psq_initiator(
// client_keypair.sk(),
// &gateway_keypair.pk,
// &enc_key,
// &salt,
// )
// .unwrap();
//
// // PSKs will be different due to randomness in PSQ, but ciphertexts too
// // This test verifies the function is deterministic given the SAME ciphertext
// let psk_responder1 = derive_psk_with_psq_responder(
// gateway_keypair.sk(),
// &client_keypair.pk,
// (&dec_key, &enc_key),
// &ct1,
// &salt,
// )
// .unwrap();
//
// let psk_responder2 = derive_psk_with_psq_responder(
// gateway_keypair.sk(),
// &client_keypair.pk,
// (&dec_key, &enc_key),
// &ct1, // Same ciphertext
// &salt,
// )
// .unwrap();
//
// assert_eq!(
// psk_responder1, psk_responder2,
// "Same ciphertext should produce same PSK"
// );
}
#[test]
fn test_psq_derivation_symmetric() {
todo!()
// let mut rng = rand09::rng();
//
// // Generate X25519 keypairs for Noise
// let client_keypair = generate_x25519_keypair();
// let gateway_keypair = generate_x25519_keypair();
//
// // Generate KEM keypair for PSQ
// let (kem_sk, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
// let enc_key = EncapsulationKey::X25519(kem_pk);
// let dec_key = DecapsulationKey::X25519(kem_sk);
//
// let salt = [2u8; 32];
//
// // Client derives PSK (initiator)
// let (client_psk, ciphertext) = derive_psk_with_psq_initiator(
// client_keypair.sk(),
// &gateway_keypair.pk,
// &enc_key,
// &salt,
// )
// .unwrap();
//
// // Gateway derives PSK from ciphertext (responder)
// let gateway_psk = derive_psk_with_psq_responder(
// gateway_keypair.sk(),
// &client_keypair.pk,
// (&dec_key, &enc_key),
// &ciphertext,
// &salt,
// )
// .unwrap();
//
// assert_eq!(
// client_psk, gateway_psk,
// "Both sides should derive identical PSK via PSQ"
// );
}
#[test]
fn test_different_kem_keys_different_psk() {
todo!()
// let mut rng = rand09::rng();
//
// let client_keypair = generate_x25519_keypair();
// let gateway_keypair = generate_x25519_keypair();
//
// // Two different KEM keypairs
// let (_, kem_pk1) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
// let (_, kem_pk2) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
//
// let enc_key1 = EncapsulationKey::X25519(kem_pk1);
// let enc_key2 = EncapsulationKey::X25519(kem_pk2);
//
// let salt = [3u8; 32];
//
// let (psk1, _) = derive_psk_with_psq_initiator(
// client_keypair.sk(),
// &gateway_keypair.pk,
// &enc_key1,
// &salt,
// )
// .unwrap();
//
// let (psk2, _) = derive_psk_with_psq_initiator(
// client_keypair.sk(),
// &gateway_keypair.pk,
// &enc_key2,
// &salt,
// )
// .unwrap();
//
// assert_ne!(
// psk1, psk2,
// "Different KEM keys should produce different PSKs"
// );
}
#[test]
fn test_psq_psk_output_length() {
todo!()
// let mut rng = rand09::rng();
//
// let client_keypair = generate_x25519_keypair();
// let gateway_keypair = generate_x25519_keypair();
//
// let (_, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
// let enc_key = EncapsulationKey::X25519(kem_pk);
//
// let salt = [4u8; 32];
//
// let (psk, _) = derive_psk_with_psq_initiator(
// client_keypair.sk(),
// &gateway_keypair.pk,
// &enc_key,
// &salt,
// )
// .unwrap();
//
// assert_eq!(psk.len(), 32, "PSQ PSK should be exactly 32 bytes");
}
#[test]
fn test_psq_different_salts_different_psks() {
todo!()
// let mut rng = rand09::rng();
//
// let client_keypair = generate_x25519_keypair();
// let gateway_keypair = generate_x25519_keypair();
//
// let (_, kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap();
// let enc_key = EncapsulationKey::X25519(kem_pk);
//
// let salt1 = [1u8; 32];
// let salt2 = [2u8; 32];
//
// let (psk1, _) = derive_psk_with_psq_initiator(
// client_keypair.sk(),
// &gateway_keypair.pk,
// &enc_key,
// &salt1,
// )
// .unwrap();
//
// let (psk2, _) = derive_psk_with_psq_initiator(
// client_keypair.sk(),
// &gateway_keypair.pk,
// &enc_key,
// &salt2,
// )
// .unwrap();
//
// assert_ne!(psk1, psk2, "Different salts should produce different PSKs");
}
}
-49
View File
@@ -1,17 +1,8 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::codec::{OuterAeadKey, parse_lp_packet, serialize_lp_packet};
use crate::{LpError, LpPacket};
use bytes::BytesMut;
use nym_lp_transport::traits::LpTransport;
use libcrux_psq::handshake::ciphersuites::CiphersuiteName;
#[cfg(test)]
use mock_instant::thread_local::{SystemTime, UNIX_EPOCH};
use nym_kkt_ciphersuite::KEM;
#[cfg(not(test))]
use std::time::{SystemTime, UNIX_EPOCH};
pub(crate) fn kem_to_ciphersuite(kem: KEM) -> CiphersuiteName {
match kem {
@@ -19,43 +10,3 @@ pub(crate) fn kem_to_ciphersuite(kem: KEM) -> CiphersuiteName {
KEM::McEliece => CiphersuiteName::X25519_CLASSICMCELIECE_X25519_AESGCM128_HKDFSHA256,
}
}
pub(crate) fn current_timestamp() -> Result<u64, LpError> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| LpError::Internal("System time before UNIX epoch".into()))
.map(|d| d.as_secs())
}
// only used in internal code (and tests)
#[allow(async_fn_in_trait)]
pub trait LpTransportHandshakeExt: LpTransport {
// the outer key is temporary until the algorithm is changed with psqv2
async fn receive_packet(
&mut self,
outer_key: Option<&OuterAeadKey>,
) -> Result<LpPacket, LpError>
where
Self: Unpin,
{
let raw = self.receive_raw_packet().await?;
parse_lp_packet(&raw, outer_key)
}
async fn send_packet(
&mut self,
packet: LpPacket,
outer_key: Option<&OuterAeadKey>,
) -> Result<(), LpError>
where
Self: Unpin,
{
let mut packet_buf = BytesMut::new();
serialize_lp_packet(&packet, &mut packet_buf, outer_key)?;
self.send_serialised_packet(&packet_buf).await?;
Ok(())
}
}
impl<T> LpTransportHandshakeExt for T where T: LpTransport {}
+23 -46
View File
@@ -1,34 +1,27 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::codec::OuterAeadKey;
use crate::message::{HandshakeData, KKTRequestData, MessageType};
use crate::noise_protocol::NoiseProtocol;
use crate::peer::{LpLocalPeer, LpRemotePeer};
use crate::psk::psq_initiator_create_message;
use crate::psq::helpers::{LpTransportHandshakeExt, current_timestamp, kem_to_ciphersuite};
use crate::psq::helpers::kem_to_ciphersuite;
use crate::psq::{
AAD_INITIATOR_INNER_V1, AAD_INITIATOR_OUTER_V1, InitiatorData, MinimalSession,
PSQHandshakeState, SESSION_CONTEXT_V1, initiator,
AAD_INITIATOR_INNER_V1, AAD_INITIATOR_OUTER_V1, InitiatorData, PSQHandshakeState,
SESSION_CONTEXT_V1,
};
use crate::session::PqSharedSecret;
use crate::{ClientHelloData, LpError, LpMessage, LpSession};
use crate::session::PersistentSessionBinding;
use crate::{LpError, LpSession};
use libcrux_psq::handshake::RegistrationInitiator;
use libcrux_psq::handshake::builders::{
CiphersuiteBuilder, InitiatorCiphersuite, PrincipalBuilder,
};
use libcrux_psq::handshake::ciphersuites::CiphersuiteName;
use libcrux_psq::handshake::types::Authenticator;
use libcrux_psq::{Channel, IntoSession};
use nym_kkt::context::KKTContext;
use nym_kkt::initiator::KKTInitiator;
use nym_kkt::keys::EncapsulationKey;
use nym_kkt::message::{KKTRequest, KKTResponse};
use nym_kkt_ciphersuite::KEM;
use nym_lp_transport::traits::LpTransport;
use rand09::rng;
use tracing::debug;
pub(crate) struct PSQHandshakeStateInitiator<'a, S> {
pub struct PSQHandshakeStateInitiator<'a, S> {
pub(super) inner_state: PSQHandshakeState<'a, S>,
pub(super) initiator_data: InitiatorData,
}
@@ -81,23 +74,6 @@ impl<'a, S> PSQHandshakeStateInitiator<'a, S>
where
S: LpTransport + Unpin,
{
fn build_psq_initiator_principal<'b>(
&'b self,
encapsulation_key: &'b EncapsulationKey,
) -> Result<RegistrationInitiator<'b, rand09::rngs::ThreadRng>, LpError> {
let initiator_ciphersuite = build_psq_ciphersuite(
&self.inner_state.local_peer,
&self.initiator_data.remote_peer,
&encapsulation_key,
)?;
let initiator = build_psq_principal(
rng(),
self.initiator_data.protocol_version,
initiator_ciphersuite,
)?;
Ok(initiator)
}
/// Attempt to send KKT request to begin the handshake
async fn send_kkt_request(&mut self, request: KKTRequest) -> Result<(), LpError> {
// TODO: extra header
@@ -114,7 +90,7 @@ where
Ok(KKTResponse::from_bytes(data))
}
pub async fn complete_handshake<R>(mut self, rng: &mut R) -> Result<MinimalSession, LpError>
pub async fn complete_handshake<R>(mut self, rng: &mut R) -> Result<LpSession, LpError>
where
S: LpTransport + Unpin,
R: rand09::CryptoRng,
@@ -143,7 +119,10 @@ where
// 4. generate and send PSQ request
let protocol = self.initiator_data.protocol_version;
let mut conn = self.inner_state.connection;
let conn = self.inner_state.connection;
// note: the clone is cheap due to internal Arcs
let encapsulation_key = response.encapsulation_key.clone();
// build the PSQ initiator
let initiator_ciphersuite = build_psq_ciphersuite(
@@ -172,12 +151,14 @@ where
));
}
let session = psq_initiator.into_session()?;
Ok(MinimalSession {
session,
encapsulation_key: Some(response.encapsulation_key),
init_authenticator: None,
})
let binding = PersistentSessionBinding {
initiator_authenticator: Authenticator::Dh(self.inner_state.local_peer.x25519().pk),
responder_ecdh_pk: self.initiator_data.remote_peer.x25519_public,
responder_pq_pk: Some(encapsulation_key),
};
let psq_session = psq_initiator.into_session()?;
LpSession::new(psq_session, binding, protocol)
}
}
@@ -186,10 +167,8 @@ mod tests {
use super::*;
use crate::peer::mock_peers;
use crate::psq::responder;
use libcrux_psq::handshake::types::Authenticator;
use libcrux_psq::session::{Session, SessionBinding};
use nym_kkt::responder::KKTResponder;
use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, SignatureScheme};
use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, KEM, SignatureScheme};
use nym_test_utils::helpers::{DeterministicRng09Send, u64_seeded_rng_09};
use nym_test_utils::mocks::async_read_write::MockIOStream;
use nym_test_utils::traits::{Leak, Timeboxed};
@@ -277,10 +256,8 @@ mod tests {
assert!(responder.is_handshake_finished());
let session_init = init_fut.await???;
let mut session_init = init_fut.await???;
let encapsulation_key = session_init.encapsulation_key.unwrap();
let mut i_transport = session_init.session;
let mut r_transport = responder.into_session().unwrap();
// test serialization, deserialization
@@ -288,7 +265,7 @@ mod tests {
let mut payload_buf_responder = vec![0u8; 4096];
let mut payload_buf_initiator = vec![0u8; 4096];
let mut channel_i = i_transport.transport_channel().unwrap();
let mut channel_i = session_init.active_transport();
let mut channel_r = r_transport.transport_channel().unwrap();
assert_eq!(channel_i.identifier(), channel_r.identifier());
+7 -33
View File
@@ -1,42 +1,27 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::LpMessage;
use crate::packet::version;
use crate::peer::{LpLocalPeer, LpRemotePeer};
use crate::psq::helpers::LpTransportHandshakeExt;
use crate::psq::initiator::PSQHandshakeStateInitiator;
use crate::psq::responder::PSQHandshakeStateResponder;
use libcrux_psq::handshake::types::Authenticator;
use libcrux_psq::session::Session;
use nym_kkt::keys::EncapsulationKey;
use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, IntoEnumIterator, SignatureScheme};
use nym_lp_transport::traits::LpTransport;
mod helpers;
mod initiator;
mod responder;
pub mod initiator;
pub mod responder;
pub use initiator::PSQHandshakeStateInitiator;
pub use responder::PSQHandshakeStateResponder;
pub(crate) const AAD_INITIATOR_OUTER_V1: &[u8] = b"NYM-PQ-AAD-INIT-OUTER-V1";
pub(crate) const AAD_INITIATOR_INNER_V1: &[u8] = b"NYM-PQ-AAD-INIT-INNER-V1";
pub(crate) const AAD_RESPONDER_V1: &[u8] = b"NYM-PQ-AAD-RESP-V1";
pub(crate) const SESSION_CONTEXT_V1: &[u8] = b"NYM-PQ-SESSION-CONTEXT-V1";
pub struct MinimalSession {
session: Session,
encapsulation_key: Option<EncapsulationKey>,
init_authenticator: Option<Authenticator>,
}
pub struct PSQHandshakeState<'a, S> {
/// The underlying connection established for the handshake
connection: &'a mut S,
/// Protocol version used for the exchange.
/// either known implicitly through the directory (initiator)
/// or established through KKTRequest (responder)
protocol_version: Option<u8>,
/// Ciphersuite selected for the KKT/PSQ exchange
ciphersuite: Ciphersuite,
@@ -94,7 +79,6 @@ where
pub fn new(connection: &'a mut S, ciphersuite: Ciphersuite, local_peer: LpLocalPeer) -> Self {
PSQHandshakeState {
connection,
protocol_version: None,
ciphersuite,
local_peer,
}
@@ -119,7 +103,6 @@ where
mod tests {
use super::*;
use crate::peer::mock_peers;
use crate::psq::helpers::LpTransportHandshakeExt;
use libcrux_psq::handshake::types::Authenticator;
use libcrux_psq::session::{Session, SessionBinding};
use libcrux_psq::{Channel, IntoSession};
@@ -133,15 +116,6 @@ mod tests {
use nym_test_utils::traits::{Leak, TimeboxedSpawnable};
use tokio::join;
#[allow(dead_code)]
async fn extract_error(conn: &mut MockIOStream) -> String {
let packet = conn.receive_packet(None).await.unwrap();
match packet.message {
LpMessage::Error(error) => error.message,
_ => panic!("non error packet"),
}
}
#[tokio::test]
async fn e2e_psq_handshake() -> anyhow::Result<()> {
let conn_init = MockIOStream::default();
@@ -183,8 +157,8 @@ mod tests {
let session_resp = session_resp???;
assert_eq!(
session_init.session.identifier(),
session_resp.session.identifier()
session_init.session_identifier(),
session_resp.session_identifier()
);
Ok(())
+33 -27
View File
@@ -1,25 +1,27 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::peer::{LpLocalPeer, LpRemotePeer};
use crate::peer::LpLocalPeer;
use crate::psq::helpers::kem_to_ciphersuite;
use crate::psq::{
AAD_RESPONDER_V1, MinimalSession, PSQHandshakeState, ResponderData, SESSION_CONTEXT_V1,
};
use crate::session::PqSharedSecret;
use crate::{ClientHelloData, LpError, LpSession};
use crate::psq::{AAD_RESPONDER_V1, PSQHandshakeState, ResponderData, SESSION_CONTEXT_V1};
use crate::session::PersistentSessionBinding;
use crate::{LpError, LpSession};
use libcrux_psq::handshake::Responder;
use libcrux_psq::handshake::builders::{
CiphersuiteBuilder, PrincipalBuilder, ResponderCiphersuite,
};
use libcrux_psq::{Channel, IntoSession};
use nym_kkt::context::KKTContext;
use nym_kkt::message::{KKTRequest, KKTResponse, ProcessedKKTRequest};
use nym_kkt::responder::KKTResponder;
use nym_kkt_ciphersuite::KEM;
use nym_lp_transport::traits::LpTransport;
use tracing::debug;
pub struct PSQHandshakeStateResponder<'a, S> {
pub(super) inner_state: PSQHandshakeState<'a, S>,
pub(super) responder_data: ResponderData,
}
pub(crate) fn build_psq_principal<R>(
rng: R,
version: u8,
@@ -44,7 +46,7 @@ where
pub(crate) fn build_psq_ciphersuite(
peer: &LpLocalPeer,
kem: KEM,
) -> Result<ResponderCiphersuite, LpError> {
) -> Result<ResponderCiphersuite<'_>, LpError> {
let Some(kem_keys) = peer.kem_keypairs.as_ref() else {
return Err(LpError::ResponderWithMissingKEMKeys);
};
@@ -64,11 +66,6 @@ pub(crate) fn build_psq_ciphersuite(
.map_err(|inner| LpError::PSQResponderBuilderFailure { inner })
}
pub(crate) struct PSQHandshakeStateResponder<'a, S> {
pub(super) inner_state: PSQHandshakeState<'a, S>,
pub(super) responder_data: ResponderData,
}
impl<'a, S> PSQHandshakeStateResponder<'a, S>
where
S: LpTransport + Unpin,
@@ -113,7 +110,7 @@ where
Ok(self.inner_state.connection.receive_raw_packet().await?)
}
pub async fn complete_handshake<R>(mut self, rng: &mut R) -> Result<MinimalSession, LpError>
pub async fn complete_handshake<R>(mut self, rng: &mut R) -> Result<LpSession, LpError>
where
S: LpTransport + Unpin,
R: rand09::CryptoRng,
@@ -144,7 +141,7 @@ where
.ok_or(LpError::MissingInitiatorAuthenticator)?;
// 4. send PSQ response
let mut conn = self.inner_state.connection;
let conn = self.inner_state.connection;
let mut buf = [0u8; 128];
let n = psq_responder.write_message(&[], &mut buf)?;
@@ -157,12 +154,25 @@ where
));
}
let session = psq_responder.into_session()?;
Ok(MinimalSession {
session,
encapsulation_key: processed_req.remote_encapsulation_key,
init_authenticator: Some(initiator_authenticator),
})
// SAFETY: we have completed the exchange so this key MUST HAVE been present
#[allow(clippy::unwrap_used)]
let kem_key = self
.inner_state
.local_peer
.kem_keypairs
.as_ref()
.unwrap()
.encapsulation_key(kem)
.unwrap();
let binding = PersistentSessionBinding {
initiator_authenticator,
responder_ecdh_pk: self.inner_state.local_peer.x25519().pk,
responder_pq_pk: Some(kem_key),
};
let psq_session = psq_responder.into_session()?;
LpSession::new(psq_session, binding, processed_req.outer_protocol_version)
}
}
@@ -171,8 +181,6 @@ mod tests {
use super::*;
use crate::peer::mock_peers;
use crate::psq::initiator;
use libcrux_psq::handshake::types::Authenticator;
use libcrux_psq::session::{Session, SessionBinding};
use nym_kkt::initiator::KKTInitiator;
use nym_kkt_ciphersuite::Ciphersuite;
use nym_test_utils::helpers::{
@@ -258,11 +266,9 @@ mod tests {
assert!(initiator.is_handshake_finished());
let session_resp = resp_fut.await???;
let init_auth = session_resp.init_authenticator.unwrap();
let mut session_resp = resp_fut.await???;
let mut i_transport = initiator.into_session().unwrap();
let mut r_transport = session_resp.session;
// test serialization, deserialization
let mut msg_channel = vec![0u8; 2048];
@@ -270,7 +276,7 @@ mod tests {
let mut payload_buf_initiator = vec![0u8; 4096];
let mut channel_i = i_transport.transport_channel().unwrap();
let mut channel_r = r_transport.transport_channel().unwrap();
let mut channel_r = session_resp.active_transport();
assert_eq!(channel_i.identifier(), channel_r.identifier());
+125 -583
View File
@@ -4,135 +4,121 @@
//! Session management for the Lewes Protocol.
//!
//! This module implements session management functionality, including replay protection
//! and Noise protocol state handling.
use crate::codec::OuterAeadKey;
use crate::message::EncryptedDataPayload;
// noiserm
use crate::noise_protocol::{NoiseError, NoiseProtocol, ReadResult};
use crate::packet::LpHeader;
use crate::codec::{decrypt_lp_packet, encrypt_lp_packet};
use crate::message::ApplicationData;
use crate::packet::{EncryptedLpPacket, LpHeader};
use crate::peer::{LpLocalPeer, LpRemotePeer};
use crate::psk::derive_subsession_psk;
use crate::replay::ReceivingKeyCounterValidator;
use crate::{LpError, LpMessage, LpPacket};
use libcrux_psq::v1::traits::PSQ;
use libcrux_psq::{Channel, IntoSession};
use rand09::rngs::ThreadRng;
use crate::psq::PSQHandshakeState;
use libcrux_psq::{
handshake::{RegistrationInitiator, Responder, types::DHPublicKey},
session::Session,
use crate::psq::{
InitiatorData, PSQHandshakeState, PSQHandshakeStateInitiator, PSQHandshakeStateResponder,
ResponderData,
};
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_kkt::context::KKTContext;
use crate::{LpError, LpMessage, LpPacket, ReceivingKeyCounterValidator};
use libcrux_psq::handshake::types::{Authenticator, DHPublicKey};
use libcrux_psq::session::{Session, SessionBinding};
use nym_kkt::keys::EncapsulationKey;
use nym_kkt_ciphersuite::{Ciphersuite, HashFunction, HashLength, KEM, SignatureScheme};
use nym_lp_transport::traits::LpTransport;
use parking_lot::Mutex;
use snow::Builder;
use zeroize::{Zeroize, ZeroizeOnDrop};
use std::fmt::{Debug, Formatter};
/// PQ shared secret wrapper with automatic memory zeroization.
/// Ensures K_pq is cleared from memory when dropped.
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct PqSharedSecret([u8; 32]);
impl PqSharedSecret {
pub fn new(secret: [u8; 32]) -> Self {
Self(secret)
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl std::fmt::Debug for PqSharedSecret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PqSharedSecret")
.field("secret", &"<redacted>")
.finish()
}
}
pub type SessionId = [u8; 32];
/// A session in the Lewes Protocol, handling connection state with Noise.
///
/// Sessions manage connection state, including LP replay protection.
/// Each session has a unique receiving index and sending index for connection identification.
#[derive(Debug)]
pub struct LpSession {
/// Id of the established session
session_id: u32,
/// The underlying established session
psq_session: Session,
/// The public key material bound to the underlying session. Used for serialisation.
session_binding: PersistentSessionBinding,
/// The current active transport channel
// In the future it might get split between UDP and TCP transports
active_transport: libcrux_psq::Transport,
/// Negotiated protocol version from handshake.
/// Set during handshake completion from the ClientHello/ServerHello packet header.
/// Used for future version negotiation and compatibility checks.
version: u8,
/// Representation of a local Lewes Protocol peer
/// encapsulating all the known information and keys.
local_peer: LpLocalPeer,
/// Representation of a remote Lewes Protocol peer
/// encapsulating all the known information and keys.
remote_peer: LpRemotePeer,
protocol_version: u8,
/// Counter for outgoing packets
sending_counter: u64,
/// Validator for incoming packet counters to prevent replay attacks
receiving_counter: ReceivingKeyCounterValidator,
}
/// Monotonically increasing counter for subsession indices.
/// Each subsession gets a unique index to ensure unique PSK derivation.
/// Uses u64 to make overflow practically impossible (~585k years at 1M/sec).
subsession_counter: u64,
/// Wraps public key material that is bound to a session.
pub struct PersistentSessionBinding {
/// The initiator's authenticator value, i.e. a long-term DH public value or signature verification key.
pub initiator_authenticator: Authenticator,
/// True if this session has been demoted to read-only mode.
/// Demoted sessions can still receive/decrypt but cannot send/encrypt.
read_only: bool,
/// The responder's long term DH public value.
pub responder_ecdh_pk: DHPublicKey,
/// ID of the successor session that replaced this one.
/// Set when demote() is called.
successor_session_id: Option<u32>,
/// The responder's long term PQ-KEM public key (if any).
pub responder_pq_pk: Option<EncapsulationKey>,
}
impl Debug for PersistentSessionBinding {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PersistentSessionBinding")
.field("initiator_authenticator", &"<initiator_authenticator>")
.field("responder_ecdh_pk", &self.responder_ecdh_pk)
.field("responder_pq_pk", &self.responder_pq_pk)
.finish()
}
}
impl<'a> From<&'a PersistentSessionBinding> for SessionBinding<'a> {
fn from(value: &'a PersistentSessionBinding) -> Self {
SessionBinding {
initiator_authenticator: &value.initiator_authenticator,
responder_ecdh_pk: &value.responder_ecdh_pk,
responder_pq_pk: value
.responder_pq_pk
.as_ref()
.map(|k| k.as_pq_encapsulation_key()),
}
}
}
impl Debug for LpSession {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LpSession")
.field("session_id", &self.psq_session.identifier())
.field("session_binding", &self.session_binding)
.field("active_transport_id", &self.active_transport.identifier())
.field("protocol_version", &self.protocol_version)
.field("sending_counter", &self.sending_counter)
.field("receiving_counter", &self.receiving_counter)
.finish()
}
}
impl LpSession {
/// Creates a new session after completed KTT/PSQ exchange
///
/// # Arguments
///
/// * `session_id` - Session identifier
/// * `version` - Protocol version to attach in all `LpPacket`s
/// * `outer_aead_key` - Outer AEAD key for packet encryption
/// * `local_peer` - This side's LP peer's keys
/// * `remote_peer` - The remote's LP peer's keys
/// * `pq_shared_secret` - Raw PQ shared secret (K_pq) from PSQ KEM encapsulation/decapsulation.
/// * `noise_state` - Noise protocol state machine
pub fn new(
session_id: u32,
version: u8,
outer_aead_key: OuterAeadKey,
local_peer: LpLocalPeer,
remote_peer: LpRemotePeer,
pq_shared_secret: PqSharedSecret,
noise_state: NoiseProtocol,
) -> Self {
todo!()
// LpSession {
// session_id,
// version,
// outer_aead_key,
// local_peer,
// remote_peer,
// pq_shared_secret,
// noise_state,
// sending_counter: 0,
// receiving_counter: Default::default(),
// subsession_counter: 0,
// read_only: false,
// successor_session_id: None,
// }
mut psq_session: Session,
session_binding: PersistentSessionBinding,
protocol_version: u8,
) -> Result<Self, LpError> {
// attempt to derive initial transport
let transport = psq_session
.transport_channel()
.map_err(|inner| LpError::TransportDerivationFailure { inner })?;
Ok(LpSession {
psq_session,
session_binding,
active_transport: transport,
protocol_version,
sending_counter: 0,
receiving_counter: Default::default(),
})
}
/// Create an instance of `Ciphersuite` using hardcoded defaults.
@@ -154,14 +140,12 @@ impl LpSession {
local_peer: LpLocalPeer,
remote_peer: LpRemotePeer,
remote_protocol_version: u8,
) -> PSQHandshakeState<'_, S>
) -> PSQHandshakeStateInitiator<'_, S>
where
S: LpTransport + Unpin,
{
todo!()
// PSQHandshakeState::new(connection, ciphersuite, local_peer)
// .with_protocol_version(remote_protocol_version)
// .with_remote_peer(remote_peer)
PSQHandshakeState::new(connection, ciphersuite, local_peer)
.as_initiator(InitiatorData::new(remote_protocol_version, remote_peer))
}
/// Helper function to create `PSQHandshakeState` for the handshake responder
@@ -169,79 +153,41 @@ impl LpSession {
connection: &'_ mut S,
ciphersuite: Ciphersuite,
local_peer: LpLocalPeer,
) -> PSQHandshakeState<'_, S>
) -> PSQHandshakeStateResponder<'_, S>
where
S: LpTransport + Unpin,
{
todo!()
// PSQHandshakeState::new(connection, ciphersuite, local_peer)
PSQHandshakeState::new(connection, ciphersuite, local_peer)
.as_responder(ResponderData::default())
}
pub fn session_binding(&self) -> &PersistentSessionBinding {
&self.session_binding
}
pub(crate) fn active_transport(&mut self) -> &mut libcrux_psq::Transport {
&mut self.active_transport
}
pub fn session_identifier(&self) -> &[u8; 32] {
self.psq_session.identifier()
}
pub fn id(&self) -> u32 {
self.session_id
todo!()
// self.session.identifier()
}
// noiserm
/// Returns the negotiated protocol version from the handshake.
///
/// Set during `LpSession` creation after sending / receiving `ClientHelloData`
pub fn negotiated_version(&self) -> u8 {
self.version
}
/// Returns the local X25519 public key.
///
/// This is used for KKT protocol when the responder needs to send their
/// KEM public key in the KKT response.
pub fn local_x25519_public(&self) -> x25519::PublicKey {
todo!()
// *self.local_peer.x25519.pk
}
/// Returns the remote ed25519 public key.
pub fn remote_ed25519_public(&self) -> ed25519::PublicKey {
self.remote_peer.ed25519_public
}
// pub fn local_kem_keys(&self) -> Result<&KemKeyPair, LpError> {
// todo!()
// // let kem = self.base.local_peer.ciphersuite.kem();
// //
// // self.base
// // .local_peer
// // .kem_key(kem)
// // .ok_or_else(|| LpError::ResponderWithMissingKEMKey { kem })
// // .map(|keys| keys.deref())
// }
/// Returns the remote X25519 public key.
///
/// Used for tie-breaking in simultaneous subsession initiation.
/// Lower key loses and becomes responder.
pub fn remote_x25519_public(&self) -> &x25519::PublicKey {
todo!()
// &self.remote_peer.x25519_public
}
// noiserm
/// Returns the outer AEAD key for packet encryption/decryption.
///
/// Returns `None` before PSK is derived (during initial handshake),
/// `Some(&OuterAeadKey)` after PSK injection via PSQ.
///
/// Callers should use `None` for packet encryption/decryption during
/// the handshake phase, and use the returned key for transport phase.
///
/// Note: For sending packets during handshake, use `outer_aead_key_for_sending()`
/// which checks PSQ state to avoid encrypting before the responder can decrypt.
pub fn outer_aead_key(&self) -> &OuterAeadKey {
todo!()
// &self.outer_aead_key
self.protocol_version
}
pub fn next_packet(&mut self, message: LpMessage) -> Result<LpPacket, LpError> {
let counter = self.next_counter();
let header = LpHeader::new(self.id(), counter, self.version);
let header = LpHeader::new(self.id(), counter, self.protocol_version, message.typ());
let packet = LpPacket::new(header, message);
Ok(packet)
}
@@ -304,299 +250,44 @@ impl LpSession {
self.receiving_counter.current_packet_cnt()
}
/// Gets the next subsession index and increments the counter.
///
/// Each subsession requires a unique index to ensure unique PSK derivation.
/// The index is monotonically increasing per session.
pub fn next_subsession_index(&mut self) -> u64 {
let next = self.subsession_counter;
self.subsession_counter += 1;
next
}
/// Attempt to retrieve expected KEM key hash of the remote
/// for [KEM] key type and [HashFunction] specified by own [KKTContext]
fn expected_kem_key_hash(&self, context: KKTContext) -> Result<&Vec<u8>, LpError> {
todo!()
// let kem = context.ciphersuite().kem();
// let hash_function = context.ciphersuite().hash_function();
//
// let digests = self
// .base
// .remote_peer
// .expected_kem_key_digests
// .get(&kem)
// .ok_or(LpError::NoKnownKEMKeyDigests { kem, hash_function })?;
//
// digests
// .get(&hash_function)
// .ok_or(LpError::NoKnownKEMKeyDigests { kem, hash_function })
}
/// Returns true if this session is in read-only mode.
///
/// Read-only sessions have been demoted after a subsession was promoted.
/// They can still decrypt incoming messages but cannot encrypt outgoing ones.
pub fn is_read_only(&self) -> bool {
self.read_only
}
/// Demotes this session to read-only mode after a subsession replaces it.
///
/// After demotion:
/// - `encrypt_data()` will return `NoiseError::SessionReadOnly`
/// - `decrypt_data()` still works (to drain in-flight messages)
/// - Session should be cleaned up after TTL expires
///
/// # Arguments
/// * `successor_idx` - The receiver index of the session that replaced this one
pub fn demote(&mut self, successor_idx: u32) {
self.successor_session_id = Some(successor_idx);
self.read_only = true;
}
/// Returns the successor session ID if this session was demoted.
pub fn successor_session_id(&self) -> Option<u32> {
self.successor_session_id
}
/// Encrypts application data payload using the established Noise transport session.
/// Encrypts a produced application using the established transport session
/// and produce an `EncryptedLpPacket`
///
/// # Arguments
///
/// * `payload` - The application data to encrypt.
/// * `data` - plaintext data to encrypt
///
/// # Returns
///
/// * `Ok(Vec<u8>)` containing the encrypted Noise message ciphertext.
/// * `Err(NoiseError)` if the session is not in transport mode or encryption fails.
pub fn encrypt_data(&mut self, payload: &[u8]) -> Result<LpMessage, NoiseError> {
// // Check if session is read-only (demoted)
// if self.read_only {
// return Err(NoiseError::SessionReadOnly);
// }
//
// let payload = self.noise_state.write_message(payload)?;
// Ok(LpMessage::EncryptedData(EncryptedDataPayload(payload)))
todo!()
/// * `Ok(EncryptedLpPacket)` containing the encrypted message ciphertext.
/// * `Err(LpError)` if the session is not in transport mode or encryption fails.
pub fn encrypt_application_data(
&mut self,
data: Vec<u8>,
) -> Result<EncryptedLpPacket, LpError> {
let packet = self.next_packet(LpMessage::ApplicationData(ApplicationData::new(data)))?;
encrypt_lp_packet(packet, &mut self.active_transport)
}
/// Decrypts an incoming Noise message containing application data.
/// Decrypts an incoming LpPacket
///
/// # Arguments
///
/// * `noise_ciphertext` - The encrypted Noise message received from the peer.
/// * `ciphertext` - The encrypted packet
///
/// # Returns
///
/// * `Ok(Vec<u8>)` containing the decrypted application data payload.
/// * `Err(NoiseError)` if the session is not in transport mode, decryption fails, or the message is not data.
pub fn decrypt_data(&mut self, noise_ciphertext: &LpMessage) -> Result<Vec<u8>, NoiseError> {
let payload = noise_ciphertext.payload();
todo!()
// match self.noise_state.read_message(payload)? {
// ReadResult::DecryptedData(data) => Ok(data),
// _ => Err(NoiseError::IncorrectStateError),
// }
}
/// Creates a new subsession using Noise KKpsk0 pattern.
///
/// KKpsk0 reuses parent's static X25519 keys (both parties know each other from parent session).
/// PSK is derived from parent's PQ shared secret, preserving quantum resistance.
///
/// # Arguments
/// * `subsession_index` - Unique index for this subsession (use `next_subsession_index()`)
/// * `is_initiator` - True if this side initiates the subsession handshake
///
/// # Returns
/// `SubsessionHandshake` ready for KK1/KK2 message exchange
///
/// # Errors
/// * Returns error if parent handshake not complete
/// * Returns error if PQ shared secret not available
pub fn create_subsession(
&self,
subsession_index: u64,
is_initiator: bool,
) -> Result<SubsessionHandshake, LpError> {
todo!()
// // Get PQ shared secret
// let pq_secret = self.pq_shared_secret();
//
// // Derive subsession PSK from parent's PQ shared secret
// let subsession_psk = derive_subsession_psk(pq_secret.as_bytes(), subsession_index);
//
// // Build KKpsk0 handshake
// // Pattern: Noise_KKpsk0_25519_ChaChaPoly_SHA256
// // Both parties already know each other's static keys from parent session
// let pattern_name = "Noise_KKpsk0_25519_ChaChaPoly_SHA256";
// let params = pattern_name.parse()?;
//
// let local_key_bytes = self.local_peer.x25519.private_key().to_bytes();
// let remote_key_bytes = self.remote_x25519_public().to_bytes();
//
// let builder = Builder::new(params)
// .local_private_key(&local_key_bytes)
// .remote_public_key(&remote_key_bytes)
// .psk(0, &subsession_psk); // PSK at position 0 for KKpsk0
//
// let handshake_state = if is_initiator {
// builder.build_initiator().map_err(LpError::SnowKeyError)?
// } else {
// builder.build_responder().map_err(LpError::SnowKeyError)?
// };
//
// Ok(SubsessionHandshake {
// index: subsession_index,
// noise_state: Mutex::new(NoiseProtocol::new(handshake_state)),
// is_initiator,
// local_peer: self.local_peer.clone(),
// remote_peer: self.remote_peer.clone(),
// pq_shared_secret: self.pq_shared_secret.clone(),
// subsession_psk,
// negotiated_version: self.version,
// })
}
}
/// Subsession created via Noise KKpsk0 handshake tunneled through parent session.
///
/// Subsessions provide fresh session keys while inheriting PQ protection from parent's
/// ML-KEM shared secret. After handshake completes, the subsession can be promoted
/// to replace the parent session.
///
/// # Lifecycle
/// 1. Parent calls `create_subsession()` to get `SubsessionHandshake`
/// 2. Initiator calls `prepare_message()` to get KK1
/// 3. KK1 sent through parent session (encrypted tunnel)
/// 4. Responder calls `process_message(kk1)` to process KK1
/// 5. Responder calls `prepare_message()` to get KK2
/// 6. KK2 sent through parent session
/// 7. Initiator calls `process_message(kk2)` to complete handshake
/// 8. Both call `is_complete()` to verify
#[derive(Debug)]
pub struct SubsessionHandshake {
/// Subsession index (unique per parent session)
pub index: u64,
/// Noise KKpsk0 handshake state
// georgio: replace with psq
noise_state: Mutex<NoiseProtocol>,
/// Is this side the initiator?
is_initiator: bool,
// Key material inherited from parent session for into_session() conversion
/// Representation of a local Lewes Protocol peer
/// encapsulating all the known information and keys.
local_peer: LpLocalPeer,
/// Representation of a remote Lewes Protocol peer
/// encapsulating all the known information and keys.
remote_peer: LpRemotePeer,
/// PQ shared secret inherited from parent (for creating further subsessions)
pq_shared_secret: PqSharedSecret,
/// Subsession PSK (for deriving outer AEAD key)
subsession_psk: [u8; 32],
/// Negotiated protocol version from handshake.
negotiated_version: u8,
}
impl SubsessionHandshake {
/// Prepares the next KK handshake message (KK1 or KK2 depending on role/state).
///
/// # Returns
/// Noise handshake message bytes to send through parent session tunnel.
pub fn prepare_message(&self) -> Result<Vec<u8>, LpError> {
let mut noise_state = self.noise_state.lock();
noise_state
.get_bytes_to_send()
.ok_or_else(|| LpError::Internal("Not our turn to send".into()))?
.map_err(LpError::NoiseError)
}
/// Processes a received KK handshake message (KK1 or KK2).
///
/// # Arguments
/// * `message` - Noise handshake message received through parent session tunnel.
///
/// # Returns
/// Any payload embedded in the handshake message (usually empty for KK).
pub fn process_message(&self, message: &[u8]) -> Result<Vec<u8>, LpError> {
let mut noise_state = self.noise_state.lock();
let result = noise_state
.read_message(message)
.map_err(LpError::NoiseError)?;
match result {
ReadResult::HandshakeComplete | ReadResult::NoOp => Ok(vec![]),
ReadResult::DecryptedData(data) => Ok(data),
}
}
/// Checks if the handshake is complete (ready for transport mode).
pub fn is_complete(&self) -> bool {
self.noise_state.lock().is_handshake_finished()
}
/// Returns whether this side is the initiator.
pub fn is_initiator(&self) -> bool {
self.is_initiator
}
/// Returns the subsession index.
pub fn subsession_index(&self) -> u64 {
self.index
}
/// Convert completed subsession handshake into a full LpSession.
///
/// This consumes the SubsessionHandshake and creates a new LpSession
/// that can be used as a replacement for the parent session.
///
/// # Arguments
/// * `receiver_index` - New receiver index for the promoted session
///
/// # Errors
/// Returns error if handshake is not complete
pub fn into_session(self, receiver_index: u32) -> Result<LpSession, LpError> {
if !self.is_complete() {
return Err(LpError::Internal(
"Cannot convert incomplete subsession to session".to_string(),
));
}
// Extract the noise state (now in transport mode)
let noise_state = self.noise_state.into_inner();
// Derive outer AEAD key from the subsession PSK
let outer_key = OuterAeadKey::from_psk(&self.subsession_psk);
todo!()
// Ok(LpSession {
// // noiserm
// session_id: receiver_index,
// noise_state,
// sending_counter: 0,
// receiving_counter: ReceivingKeyCounterValidator::new(0),
// local_peer: self.local_peer,
// remote_peer: self.remote_peer,
// outer_aead_key: outer_key,
// pq_shared_secret: self.pq_shared_secret,
// subsession_counter: 0,
// read_only: false,
// successor_session_id: None,
// version: self.negotiated_version,
// })
/// * `Ok(LpPacket)` containing the decrypted application data payload.
/// * `Err(LpError)` if the session is not in transport mode, decryption fails, or the message is not data.
pub fn decrypt_packet(&mut self, packet: EncryptedLpPacket) -> Result<LpPacket, LpError> {
decrypt_lp_packet(packet, &mut self.active_transport)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{SessionsMock, kem_list, replay::ReplayError, sessions_for_tests};
use nym_crypto::asymmetric::x25519;
use rand::thread_rng;
// Helper function to generate keypairs for tests
@@ -621,21 +312,6 @@ mod tests {
// }
}
// NOTE: These tests are obsolete after removing optional KEM parameters.
// PSQ now always runs using X25519 keys internally converted to KEM format.
// The new tests at the end of this file (test_psq_*) cover PSQ integration.
/*
#[test]
fn test_session_creation_with_psq_state_initiator() {
// OLD API - REMOVED
}
#[test]
fn test_session_creation_with_psq_state_responder() {
// OLD API - REMOVED
}
*/
#[test]
fn test_replay_protection_sequential() {
todo!()
@@ -705,138 +381,4 @@ mod tests {
// assert_eq!(received, 2);
// }
}
/*
// These tests remain commented as they rely on the old mock crypto functions
#[test]
fn test_mock_crypto() {
let mut session = create_test_session(true);
let data = [1, 2, 3, 4, 5];
let mut encrypted = [0; 5];
let mut decrypted = [0; 5];
// Mock encrypt should copy the data
// let encrypted_len = session.encrypt_packet(&data, &mut encrypted).unwrap(); // Removed method
// assert_eq!(encrypted_len, 5);
// assert_eq!(encrypted, data);
// Mock decrypt should copy the data
// let decrypted_len = session.decrypt_packet(&encrypted, &mut decrypted).unwrap(); // Removed method
// assert_eq!(decrypted_len, 5);
// assert_eq!(decrypted, data);
}
#[test]
fn test_mock_crypto_buffer_too_small() {
let mut session = create_test_session(true);
let data = [1, 2, 3, 4, 5];
let mut too_small = [0; 3];
// Should fail with buffer too small
// let result = session.encrypt_packet(&data, &mut too_small); // Removed method
// assert!(result.is_err());
// match result.unwrap_err() {
// LpError::InsufficientBufferSize => {} // Error type might change
// _ => panic!("Expected InsufficientBufferSize error"),
// }
}
*/
/// Test that X25519 keys are correctly converted to KEM format
#[test]
fn test_x25519_to_kem_conversion() {
todo!()
//
// let initiator_keys = generate_x25519_keypair();
// let responder_keys = generate_x25519_keypair();
//
// // Verify we can convert X25519 public key to KEM format (as done in session.rs)
// let x25519_public_bytes = responder_keys.public_key().as_bytes();
// let libcrux_public_key =
// libcrux_kem::PublicKey::decode(libcrux_kem::Algorithm::X25519, x25519_public_bytes)
// .expect("X25519 public key should convert to libcrux PublicKey");
//
// let _kem_key = EncapsulationKey::X25519(libcrux_public_key);
//
// // Verify we can convert X25519 private key to KEM format
// let x25519_private_bytes = initiator_keys.private_key().to_bytes();
// let _libcrux_private_key =
// libcrux_kem::PrivateKey::decode(libcrux_kem::Algorithm::X25519, &x25519_private_bytes)
// .expect("X25519 private key should convert to libcrux PrivateKey");
//
// // Successful conversion is sufficient - actual encapsulation is tested in psk.rs
// // (libcrux_kem::PrivateKey is an enum with no len() method, conversion success is enough)
}
#[test]
fn test_demote_sets_read_only() {
todo!()
// let sessions = SessionsMock::mock_post_handshake(12345);
// let mut session = sessions.initiator;
// for kem in kem_list() {
// let session = create_handshake_test_session(kem, 12345u32, true);
//
// // Initially not read-only
// assert!(!session.is_read_only());
// assert!(session.successor_session_id().is_none());
//
// // Demote the session
// session.demote(99999);
//
// // Now read-only with successor
// assert!(session.is_read_only());
// assert_eq!(session.successor_session_id(), Some(99999));
// }
}
#[test]
fn test_encrypt_fails_after_demotion() {
let TODO = " for kem in kem_list() {";
let receiver_index = 12345;
let sessions = SessionsMock::mock_post_handshake(receiver_index);
let mut initiator_session = sessions.initiator;
// Encryption works before demotion
let plaintext = b"Hello before demotion";
assert!(initiator_session.encrypt_data(plaintext).is_ok());
// Demote the session
initiator_session.demote(99999);
// Encryption fails after demotion
let result = initiator_session.encrypt_data(plaintext);
assert!(result.is_err());
match result.unwrap_err() {
NoiseError::SessionReadOnly => {
// Expected
}
e => panic!("Expected SessionReadOnly error, got: {:?}", e),
}
}
#[test]
fn test_decrypt_works_after_demotion() {
// --- Setup Handshake ---
let TODO = " for kem in kem_list() {";
let receiver_index = 12345;
let sessions = SessionsMock::mock_post_handshake(receiver_index);
let mut initiator_session = sessions.initiator;
let mut responder_session = sessions.responder;
// Responder encrypts a message
let plaintext = b"Message to demoted initiator";
let ciphertext = responder_session
.encrypt_data(plaintext)
.expect("Encryption failed");
// Demote the initiator session
initiator_session.demote(99999);
assert!(initiator_session.is_read_only());
// Decryption still works on demoted session (drain in-flight)
let decrypted = initiator_session
.decrypt_data(&ciphertext)
.expect("Decryption should work on demoted session");
assert_eq!(decrypted, plaintext);
}
}
File diff suppressed because it is too large Load Diff
+29 -40
View File
@@ -6,20 +6,17 @@
//! This module implements session lifecycle management functionality, handling
//! creation, retrieval, and storage of sessions.
use crate::peer::{LpLocalPeer, LpRemotePeer};
use crate::state_machine::{LpAction, LpInput, LpState, LpStateBare};
use crate::{LpError, LpMessage, LpSession, LpStateMachine};
use crate::session::SessionId;
use crate::state_machine::{LpAction, LpInput, LpStateBare};
use crate::{LpError, LpSession, LpStateMachine};
use std::collections::HashMap;
#[cfg(test)]
use libcrux_psq::handshake::types::DHPublicKey;
/// Manages the lifecycle of Lewes Protocol sessions.
///
/// The SessionManager is responsible for creating, storing, and retrieving sessions
pub struct SessionManager {
/// Manages state machines directly, keyed by lp_id
state_machines: HashMap<u32, LpStateMachine>,
state_machines: HashMap<SessionId, LpStateMachine>,
}
impl Default for SessionManager {
@@ -38,62 +35,54 @@ impl SessionManager {
pub fn process_input(
&mut self,
lp_id: u32,
lp_id: SessionId,
input: LpInput,
) -> Result<Option<LpAction>, LpError> {
self.with_state_machine_mut(lp_id, |sm| sm.process_input(input).transpose())?
}
pub fn closed(&self, lp_id: u32) -> Result<bool, LpError> {
pub fn closed(&self, lp_id: SessionId) -> Result<bool, LpError> {
Ok(self.get_state(lp_id)? == LpStateBare::Closed)
}
pub fn transport(&self, lp_id: u32) -> Result<bool, LpError> {
pub fn transport(&self, lp_id: SessionId) -> Result<bool, LpError> {
Ok(self.get_state(lp_id)? == LpStateBare::Transport)
}
#[cfg(test)]
fn get_state_machine_id(&self, lp_id: u32) -> Result<u32, LpError> {
fn get_state_machine_id(&self, lp_id: SessionId) -> Result<SessionId, LpError> {
self.with_state_machine(lp_id, |sm| sm.id())?
}
pub fn get_state(&self, lp_id: u32) -> Result<LpStateBare, LpError> {
pub fn get_state(&self, lp_id: SessionId) -> Result<LpStateBare, LpError> {
self.with_state_machine(lp_id, |sm| Ok(sm.bare_state()))?
}
pub fn receiving_counter_quick_check(&self, lp_id: u32, counter: u64) -> Result<(), LpError> {
pub fn receiving_counter_quick_check(
&self,
lp_id: SessionId,
counter: u64,
) -> Result<(), LpError> {
self.with_state_machine(lp_id, |sm| {
sm.session()?.receiving_counter_quick_check(counter)
})?
}
pub fn receiving_counter_mark(&mut self, lp_id: u32, counter: u64) -> Result<(), LpError> {
pub fn receiving_counter_mark(
&mut self,
lp_id: SessionId,
counter: u64,
) -> Result<(), LpError> {
self.with_state_machine_mut(lp_id, |sm| {
sm.session_mut()?.receiving_counter_mark(counter)
})?
}
pub fn next_counter(&mut self, lp_id: u32) -> Result<u64, LpError> {
pub fn next_counter(&mut self, lp_id: SessionId) -> Result<u64, LpError> {
self.with_state_machine_mut(lp_id, |sm| Ok(sm.session_mut()?.next_counter()))?
}
pub fn decrypt_data(&mut self, lp_id: u32, message: &LpMessage) -> Result<Vec<u8>, LpError> {
self.with_state_machine_mut(lp_id, |sm| {
sm.session_mut()?
.decrypt_data(message)
.map_err(LpError::NoiseError)
})?
}
pub fn encrypt_data(&mut self, lp_id: u32, message: &[u8]) -> Result<LpMessage, LpError> {
self.with_state_machine_mut(lp_id, |sm| {
sm.session_mut()?
.encrypt_data(message)
.map_err(LpError::NoiseError)
})?
}
pub fn current_packet_cnt(&self, lp_id: u32) -> Result<(u64, u64), LpError> {
pub fn current_packet_cnt(&self, lp_id: SessionId) -> Result<(u64, u64), LpError> {
self.with_state_machine(lp_id, |sm| Ok(sm.session()?.current_packet_cnt()))?
}
@@ -101,11 +90,11 @@ impl SessionManager {
self.state_machines.len()
}
pub fn state_machine_exists(&self, lp_id: u32) -> bool {
pub fn state_machine_exists(&self, lp_id: SessionId) -> bool {
self.state_machines.contains_key(&lp_id)
}
pub fn with_state_machine<F, R>(&self, lp_id: u32, f: F) -> Result<R, LpError>
pub fn with_state_machine<F, R>(&self, lp_id: SessionId, f: F) -> Result<R, LpError>
where
F: FnOnce(&LpStateMachine) -> R,
{
@@ -117,7 +106,7 @@ impl SessionManager {
}
// For mutable access (like running process_input)
pub fn with_state_machine_mut<F, R>(&mut self, lp_id: u32, f: F) -> Result<R, LpError>
pub fn with_state_machine_mut<F, R>(&mut self, lp_id: SessionId, f: F) -> Result<R, LpError>
where
F: FnOnce(&mut LpStateMachine) -> R, // Closure takes mutable ref
{
@@ -128,15 +117,15 @@ impl SessionManager {
}
}
pub fn create_session_state_machine(&mut self, lp_session: LpSession) -> u32 {
let receiver_index = lp_session.id();
pub fn create_session_state_machine(&mut self, lp_session: LpSession) -> SessionId {
let session_id = *lp_session.session_identifier();
let sm = LpStateMachine::new(lp_session);
self.state_machines.insert(receiver_index, sm);
receiver_index
self.state_machines.insert(session_id, sm);
session_id
}
/// Method to remove a state machine
pub fn remove_state_machine(&mut self, lp_id: u32) -> bool {
pub fn remove_state_machine(&mut self, lp_id: SessionId) -> bool {
let removed = self.state_machines.remove(&lp_id);
removed.is_some()
File diff suppressed because it is too large Load Diff
+7 -15
View File
@@ -6,6 +6,7 @@ use crate::error::GatewayError;
use bytes::BytesMut;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_lp::codec::serialize_lp_packet;
use nym_lp::message::ApplicationData;
use nym_lp::state_machine::{LpAction, LpData, LpDataKind, LpInput};
use nym_lp::{
message::ForwardPacketData, LpMessage, LpPacket, LpSession, LpStateMachine, OuterHeader,
@@ -512,23 +513,14 @@ where
let wrapped_lp_data = LpData::new(response_kind, serialised_response);
let data_bytes = wrapped_lp_data.to_vec();
let encrypted_message = session.encrypt_data(&data_bytes).map_err(|e| {
let encrypted_message = session.encrypt_application_data(data_bytes).map_err(|e| {
GatewayError::LpProtocolError(format!("Failed to encrypt response: {e}"))
})?;
let response_packet = session.next_packet(encrypted_message).map_err(|e| {
GatewayError::LpProtocolError(format!("Failed to create response packet: {e}"))
})?;
let outer_key = session.outer_aead_key();
// make sure to drop the entry before the .await call
// Serialize the packet (encrypted if outer_key provided)
let mut packet_buf = BytesMut::new();
serialize_lp_packet(&response_packet, &mut packet_buf, Some(outer_key)).map_err(|e| {
GatewayError::LpProtocolError(format!("Failed to serialize packet: {}", e))
})?;
drop(session_entry);
encrypted_message.encode(&mut packet_buf);
// Send response (encrypted with outer AEAD)
self.send_serialised_packet(&packet_buf).await?;
@@ -1038,7 +1030,7 @@ mod tests {
use crate::node::ActiveClientsStore;
use bytes::BytesMut;
use nym_lp::codec::{parse_lp_packet, serialize_lp_packet, OuterAeadKey};
use nym_lp::message::{EncryptedDataPayload, LpMessage};
use nym_lp::message::{ApplicationData, LpMessage};
use nym_lp::packet::{LpHeader, LpPacket};
use nym_lp::peer::LpLocalPeer;
use nym_lp::SessionsMock;
@@ -1404,7 +1396,7 @@ mod tests {
receiver_idx,
counter: 20,
},
LpMessage::EncryptedData(EncryptedDataPayload(encrypted_payload)),
LpMessage::ApplicationData(ApplicationData(encrypted_payload)),
);
handler.send_lp_packet(packet).await
});
@@ -1418,8 +1410,8 @@ mod tests {
assert_eq!(received.header().receiver_idx, 200);
assert_eq!(received.header().counter, 20);
match received.message() {
LpMessage::EncryptedData(data) => {
assert_eq!(data, &EncryptedDataPayload(expected_payload))
LpMessage::ApplicationData(data) => {
assert_eq!(data, &ApplicationData(expected_payload))
}
_ => panic!("Expected EncryptedData message"),
}