LpSession cleanup
This commit is contained in:
+7
-7
@@ -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
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+11
-25
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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(×tamp.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"),
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
+171
-908
File diff suppressed because it is too large
Load Diff
@@ -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"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user