Lp/stateless handshake (#6437)
* perform KKT/PSQ handshake outside of LPStateMachine * initiator * responder * concurrent test * remove KTT/PSQ from the LpStateMachine * adjusted gateway's Handler to accomodate new changes * filling in placehlders * fixed imports in nym-kkt crate * naming * clippy and moved more placeholder tests * split up the initiator side of the PSQ * split up the responder side of the PSQ * additional helpers * addressing review comments * additional tests and explicit Error message
This commit is contained in:
committed by
GitHub
parent
9cb2655e7d
commit
bb694855d5
Generated
+4
-1
@@ -6925,6 +6925,7 @@ dependencies = [
|
||||
name = "nym-lp"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bs58",
|
||||
"bytes",
|
||||
"chacha20poly1305",
|
||||
@@ -6933,20 +6934,22 @@ dependencies = [
|
||||
"libcrux-kem",
|
||||
"libcrux-psq",
|
||||
"libcrux-traits",
|
||||
"mock_instant",
|
||||
"num_enum",
|
||||
"nym-crypto",
|
||||
"nym-kkt",
|
||||
"nym-lp-common",
|
||||
"nym-lp-transport",
|
||||
"nym-test-utils",
|
||||
"parking_lot",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.2",
|
||||
"rand_chacha 0.3.1",
|
||||
"serde",
|
||||
"sha2 0.10.9",
|
||||
"snow",
|
||||
"thiserror 2.0.17",
|
||||
"tls_codec",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
+4
-3
@@ -320,6 +320,7 @@ publicsuffix = "2.3.0"
|
||||
proc_pidinfo = "0.1.3"
|
||||
quote = "1"
|
||||
rand = "0.8.5"
|
||||
rand09 = { package = "rand", version = "=0.9.2" }
|
||||
rand_chacha = "0.3"
|
||||
rand_core = "0.6.3"
|
||||
rand_distr = "0.4"
|
||||
@@ -483,9 +484,9 @@ nym-vesting-contract-common = { version = "1.20.4", path = "common/cosmwasm-smar
|
||||
nym-verloc = { version = "1.20.4", path = "common/verloc" }
|
||||
nym-wireguard = { version = "1.20.4", path = "common/wireguard" }
|
||||
nym-wireguard-types = { version = "1.20.4", path = "common/wireguard-types" }
|
||||
nym-wireguard-private-metadata-shared = { version = "1.20.4", path = "common/wireguard-private-metadata/shared" }
|
||||
nym-wireguard-private-metadata-client = { version = "1.20.4", path = "common/wireguard-private-metadata/client" }
|
||||
nym-wireguard-private-metadata-server = { version = "1.20.4", path = "common/wireguard-private-metadata/server" }
|
||||
nym-wireguard-private-metadata-shared = { version = "1.20.4", path = "common/wireguard-private-metadata/shared" }
|
||||
nym-wireguard-private-metadata-client = { version = "1.20.4", path = "common/wireguard-private-metadata/client" }
|
||||
nym-wireguard-private-metadata-server = { version = "1.20.4", path = "common/wireguard-private-metadata/server" }
|
||||
nym-sqlx-pool-guard = { version = "1.2.0", path = "nym-sqlx-pool-guard" }
|
||||
nym-wasm-client-core = { version = "1.20.4", path = "common/wasm/client-core" }
|
||||
nym-wasm-storage = { version = "1.20.4", path = "common/wasm/storage" }
|
||||
|
||||
@@ -10,7 +10,7 @@ pub use opentelemetry;
|
||||
pub use opentelemetry_jaeger;
|
||||
#[cfg(feature = "tracing")]
|
||||
pub use tracing_opentelemetry;
|
||||
#[cfg(feature = "tracing")]
|
||||
#[cfg(feature = "basic_tracing")]
|
||||
pub use tracing_subscriber;
|
||||
#[cfg(feature = "tracing")]
|
||||
pub use tracing_tree;
|
||||
|
||||
@@ -33,7 +33,7 @@ thiserror = { workspace = true }
|
||||
zeroize = { workspace = true, optional = true, features = ["zeroize_derive"] }
|
||||
|
||||
# internal
|
||||
nym-sphinx-types = { workspace = true }
|
||||
nym-sphinx-types = { workspace = true, optional = true }
|
||||
nym-pemstore = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -51,7 +51,7 @@ serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"
|
||||
asymmetric = ["x25519-dalek", "ed25519-dalek", "curve25519-dalek", "sha2", "zeroize"]
|
||||
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2", "zeroize"]
|
||||
stream_cipher = ["aes", "ctr", "cipher", "generic-array"]
|
||||
sphinx = ["nym-sphinx-types/sphinx"]
|
||||
sphinx = ["nym-sphinx-types", "nym-sphinx-types/sphinx"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -21,7 +21,8 @@ libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", features = ["codec"] }
|
||||
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux" }
|
||||
|
||||
rand = "0.9.2"
|
||||
# rand 0.9 for libcrux integration (libcrux uses rand 0.9)
|
||||
rand09 = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
classic-mceliece-rust = { git = "https://github.com/georgio/classic-mceliece-rust", features = ["mceliece460896f", "zeroize"] }
|
||||
|
||||
|
||||
@@ -18,13 +18,13 @@ use nym_kkt::{
|
||||
responder_ingest_message, responder_process,
|
||||
},
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use rand09::prelude::*;
|
||||
|
||||
pub fn gen_ed25519_keypair(c: &mut Criterion) {
|
||||
c.bench_function("Generate Ed25519 Keypair", |b| {
|
||||
b.iter(|| {
|
||||
let mut s: [u8; 32] = [0u8; 32];
|
||||
rand::rng().fill_bytes(&mut s);
|
||||
rand09::rng().fill_bytes(&mut s);
|
||||
ed25519::KeyPair::from_secret(s, 0)
|
||||
});
|
||||
});
|
||||
@@ -33,13 +33,13 @@ pub fn gen_ed25519_keypair(c: &mut Criterion) {
|
||||
pub fn gen_mlkem768_keypair(c: &mut Criterion) {
|
||||
c.bench_function("Generate MlKem768 Keypair", |b| {
|
||||
b.iter(|| {
|
||||
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand::rng()).unwrap()
|
||||
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand09::rng()).unwrap()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// generate ed25519 keys
|
||||
let mut secret_initiator: [u8; 32] = [0u8; 32];
|
||||
@@ -111,7 +111,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
},
|
||||
);
|
||||
|
||||
let (mut i_context, i_frame) =
|
||||
let (i_context, i_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
@@ -143,7 +143,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
},
|
||||
);
|
||||
|
||||
let (mut r_context, _) =
|
||||
let (r_context, _) =
|
||||
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
@@ -153,7 +153,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -163,7 +163,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
},
|
||||
);
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -184,7 +184,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -196,7 +196,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
);
|
||||
|
||||
let obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -208,7 +208,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
@@ -262,7 +262,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
},
|
||||
);
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
@@ -279,7 +279,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -290,7 +290,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -311,7 +311,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -323,7 +323,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
);
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -352,7 +352,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
},
|
||||
);
|
||||
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
@@ -394,7 +394,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
},
|
||||
);
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
@@ -411,7 +411,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -422,7 +422,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -445,7 +445,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -457,7 +457,7 @@ pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
);
|
||||
|
||||
let obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&r_frame,
|
||||
&r_frame.context().unwrap(),
|
||||
responder_ed25519_keypair.public_key(),
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{KKT_INITIAL_FRAME_AAD, context::KKTContext, error::KKTError, frame::
|
||||
use blake3::Hasher;
|
||||
use libcrux_chacha20poly1305::{NONCE_LEN, TAG_LEN};
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
#[derive(Clone, Copy, Zeroize)]
|
||||
@@ -182,8 +182,7 @@ mod test {
|
||||
encryption::{KKTSessionSecret, decrypt, encrypt},
|
||||
key_utils::generate_keypair_x25519,
|
||||
};
|
||||
use rand::{RngCore, SeedableRng, rng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use rand09::{RngCore, SeedableRng, rng};
|
||||
|
||||
#[test]
|
||||
fn test_keygen() {
|
||||
@@ -227,7 +226,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn kkt_frame_encryption() -> anyhow::Result<()> {
|
||||
let mut rng = ChaCha20Rng::seed_from_u64(42);
|
||||
let mut rng = rand_chacha::ChaCha20Rng::seed_from_u64(42);
|
||||
let session_key = KKTSessionSecret::from_bytes([42u8; 32]);
|
||||
let aad = b"my-amazing-aad";
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::collections::HashMap;
|
||||
use classic_mceliece_rust::keypair_boxed;
|
||||
|
||||
use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, KeyDigests};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
|
||||
pub fn generate_keypair_ed25519<R>(
|
||||
rng: &mut R,
|
||||
@@ -61,7 +61,6 @@ pub fn generate_keypair_mceliece<'a, R>(
|
||||
classic_mceliece_rust::PublicKey<'a>,
|
||||
)
|
||||
where
|
||||
// this is annoying because mceliece lib uses rand 0.8.5...
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
let (encapsulation_key, decapsulation_key) = keypair_boxed(rng);
|
||||
|
||||
+14
-15
@@ -9,7 +9,7 @@
|
||||
//! The underlying KKT protocol is implemented in the `session` module.
|
||||
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
|
||||
use crate::{
|
||||
ciphersuite::{Ciphersuite, EncapsulationKey},
|
||||
@@ -33,7 +33,7 @@ use crate::frame::KKTFrame;
|
||||
/// The request will be signed with the provided signing key.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `rng` - Random number generator
|
||||
/// * `rng` - random number generator
|
||||
/// * `ciphersuite` - Negotiated ciphersuite (KEM, hash, signature algorithms)
|
||||
/// * `signing_key` - Client's Ed25519 signing key for authentication
|
||||
/// * `responder_dh_public_key` - Responder's long-term x25519 Diffie-Hellman public key
|
||||
@@ -90,7 +90,7 @@ pub fn request_kem_key<R: CryptoRng + RngCore>(
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// let gateway_kem_key = validate_kem_response(
|
||||
/// &mut context,
|
||||
/// &context,
|
||||
/// &session_secret,
|
||||
/// &gateway_verification_key,
|
||||
/// &expected_hash_from_directory,
|
||||
@@ -199,14 +199,14 @@ mod tests {
|
||||
|
||||
fn random_x25519_key() -> x25519::PrivateKey {
|
||||
let mut bytes = [0u8; 32];
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
rng.fill_bytes(&mut bytes);
|
||||
x25519::PrivateKey::from_secret(bytes)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kkt_wrappers_oneway_authenticated() {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// Generate Ed25519 keypairs for both parties
|
||||
let mut initiator_secret = [0u8; 32];
|
||||
@@ -241,7 +241,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Client: Request KEM key
|
||||
let (session_key, mut context, request_frame_ciphertext) = request_kem_key(
|
||||
let (session_key, context, request_frame_ciphertext) = request_kem_key(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
ed25519_init.private_key(),
|
||||
@@ -262,7 +262,7 @@ mod tests {
|
||||
|
||||
// Client: Validate response
|
||||
let obtained_key = validate_kem_response(
|
||||
&mut context,
|
||||
&context,
|
||||
&session_key,
|
||||
ed25519_resp.public_key(),
|
||||
&key_hash,
|
||||
@@ -276,7 +276,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_kkt_wrappers_anonymous() {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// Only responder has keys
|
||||
let mut responder_secret = [0u8; 32];
|
||||
@@ -304,8 +304,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Anonymous initiator
|
||||
let (mut context, request_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
let (context, request_frame) = anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
// Generate the session's shared secret and encrypt the Initiator's request
|
||||
let (session_secret, encrypted_request_bytes) =
|
||||
@@ -324,7 +323,7 @@ mod tests {
|
||||
|
||||
// Initiator: Validate response
|
||||
let obtained_key = validate_kem_response(
|
||||
&mut context,
|
||||
&context,
|
||||
&session_secret,
|
||||
responder_keypair.public_key(),
|
||||
&key_hash,
|
||||
@@ -337,7 +336,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_invalid_signature_rejected() {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
let mut initiator_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut initiator_secret);
|
||||
@@ -390,7 +389,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_hash_mismatch_rejected() {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
let mut initiator_secret = [0u8; 32];
|
||||
rng.fill_bytes(&mut initiator_secret);
|
||||
@@ -417,7 +416,7 @@ mod tests {
|
||||
// Use WRONG hash
|
||||
let wrong_hash = [0u8; 32];
|
||||
|
||||
let (session_key, mut context, request_frame) = request_kem_key(
|
||||
let (session_key, context, request_frame) = request_kem_key(
|
||||
&mut rng,
|
||||
ciphersuite,
|
||||
initiator_keypair.private_key(),
|
||||
@@ -437,7 +436,7 @@ mod tests {
|
||||
|
||||
// Client validates with WRONG hash
|
||||
let result = validate_kem_response(
|
||||
&mut context,
|
||||
&context,
|
||||
&session_key,
|
||||
responder_keypair.public_key(),
|
||||
&wrong_hash, // Wrong!
|
||||
|
||||
+26
-26
@@ -38,7 +38,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_kkt_psq_e2e_clear() {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// generate ed25519 keys
|
||||
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
||||
@@ -106,18 +106,18 @@ mod test {
|
||||
|
||||
// Anonymous Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) =
|
||||
let (i_context, i_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
let (mut r_context, _) =
|
||||
let (r_context, _) =
|
||||
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -129,7 +129,7 @@ mod test {
|
||||
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -141,7 +141,7 @@ mod test {
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
@@ -154,7 +154,7 @@ mod test {
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
@@ -165,7 +165,7 @@ mod test {
|
||||
assert!(r_obtained_key.is_none());
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -177,7 +177,7 @@ mod test {
|
||||
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -190,7 +190,7 @@ mod test {
|
||||
|
||||
// Initiator, Mutual
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
@@ -203,7 +203,7 @@ mod test {
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
@@ -214,7 +214,7 @@ mod test {
|
||||
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -226,7 +226,7 @@ mod test {
|
||||
let (i_frame_r, i_context_r) = KKTFrame::from_bytes(&r_bytes).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -241,7 +241,7 @@ mod test {
|
||||
}
|
||||
#[test]
|
||||
fn test_kkt_psq_e2e_encrypted() {
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand09::rng();
|
||||
|
||||
// generate ed25519 keys
|
||||
let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0));
|
||||
@@ -312,7 +312,7 @@ mod test {
|
||||
|
||||
// Anonymous Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) =
|
||||
let (i_context, i_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
// encryption - initiator frame
|
||||
@@ -330,11 +330,11 @@ mod test {
|
||||
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
||||
.unwrap();
|
||||
|
||||
let (mut r_context, _) =
|
||||
let (r_context, _) =
|
||||
responder_ingest_message(&i_context_r, None, None, &i_frame_r).unwrap();
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -352,7 +352,7 @@ mod test {
|
||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -364,7 +364,7 @@ mod test {
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
@@ -388,7 +388,7 @@ mod test {
|
||||
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
||||
.unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
@@ -399,7 +399,7 @@ mod test {
|
||||
assert!(r_obtained_key.is_none());
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -417,7 +417,7 @@ mod test {
|
||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
@@ -430,7 +430,7 @@ mod test {
|
||||
|
||||
// Initiator, Mutual
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
let (i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
@@ -454,7 +454,7 @@ mod test {
|
||||
decrypt_initial_kkt_frame(responder_x25519_keypair.private_key(), &i_bytes)
|
||||
.unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
let (r_context, r_obtained_key) = responder_ingest_message(
|
||||
&i_context_r,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
@@ -465,7 +465,7 @@ mod test {
|
||||
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
&r_context,
|
||||
i_frame_r.session_id(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
@@ -483,7 +483,7 @@ mod test {
|
||||
decrypt_kkt_frame(&i_session_secret, &r_bytes, KKT_RESPONSE_AAD).unwrap();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
&i_context,
|
||||
&i_frame_r,
|
||||
&i_context_r,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use nym_crypto::asymmetric::ed25519::{self, Signature};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use rand09::{CryptoRng, RngCore};
|
||||
|
||||
use crate::frame::KKTSessionId;
|
||||
use crate::{
|
||||
@@ -73,7 +73,7 @@ where
|
||||
}
|
||||
|
||||
pub fn initiator_ingest_response<'a>(
|
||||
own_context: &mut KKTContext,
|
||||
own_context: &KKTContext,
|
||||
remote_frame: &KKTFrame,
|
||||
remote_context: &KKTContext,
|
||||
remote_verification_key: &ed25519::PublicKey,
|
||||
@@ -201,7 +201,7 @@ pub fn responder_ingest_message<'a>(
|
||||
}
|
||||
|
||||
pub fn responder_process<'a>(
|
||||
own_context: &mut KKTContext,
|
||||
own_context: &KKTContext,
|
||||
session_id: KKTSessionId,
|
||||
signing_key: &ed25519::PrivateKey,
|
||||
encapsulation_key: &EncapsulationKey<'a>,
|
||||
|
||||
@@ -10,7 +10,7 @@ use tracing::debug;
|
||||
|
||||
// only used in internal code (and tests)
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait LpTransport: AsyncRead + AsyncWrite + Sized {
|
||||
pub trait LpTransport: Sized {
|
||||
async fn connect(endpoint: SocketAddr) -> std::io::Result<Self>;
|
||||
|
||||
fn set_no_delay(&mut self, nodelay: bool) -> std::io::Result<()>;
|
||||
@@ -24,32 +24,7 @@ pub trait LpTransport: AsyncRead + AsyncWrite + Sized {
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error on network transmission fails.
|
||||
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()>
|
||||
where
|
||||
Self: Unpin,
|
||||
{
|
||||
// Send 4-byte length prefix (u32 big-endian)
|
||||
let len = packet_data.len() as u32;
|
||||
self.write_all(&len.to_be_bytes())
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to send packet length: {e}"))?;
|
||||
|
||||
// Send the actual packet data
|
||||
self.write_all(packet_data)
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to send packet data: {e}"))?;
|
||||
|
||||
// Flush to ensure data is sent immediately
|
||||
self.flush()
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to flush stream: {e}"))?;
|
||||
|
||||
tracing::trace!(
|
||||
"Sent LP packet ({} bytes + 4 byte header)",
|
||||
packet_data.len()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()>;
|
||||
|
||||
/// Receives an LP packet from a TCP stream with length-prefixed framing.
|
||||
///
|
||||
@@ -57,35 +32,72 @@ pub trait LpTransport: AsyncRead + AsyncWrite + Sized {
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error on network transmission fails.
|
||||
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>>
|
||||
where
|
||||
Self: Unpin,
|
||||
{
|
||||
// Read 4-byte length prefix (u32 big-endian)
|
||||
let mut len_buf = [0u8; 4];
|
||||
self.read_exact(&mut len_buf)
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to read packet length: {e}"))?;
|
||||
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
let packet_len = u32::from_be_bytes(len_buf) as usize;
|
||||
async fn send_serialised_packet_async_write<W>(
|
||||
writer: &mut W,
|
||||
packet_data: &[u8],
|
||||
) -> std::io::Result<()>
|
||||
where
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
// Send 4-byte length prefix (u32 big-endian)
|
||||
let len = packet_data.len() as u32;
|
||||
writer
|
||||
.write_all(&len.to_be_bytes())
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to send packet length: {e}"))?;
|
||||
|
||||
// Sanity check to prevent huge allocations
|
||||
const MAX_PACKET_SIZE: usize = 65536; // 64KB max
|
||||
if packet_len > MAX_PACKET_SIZE {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"Packet size {packet_len} exceeds maximum {MAX_PACKET_SIZE}",
|
||||
)));
|
||||
}
|
||||
// Send the actual packet data
|
||||
writer
|
||||
.write_all(packet_data)
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to send packet data: {e}"))?;
|
||||
|
||||
// Read the actual packet data
|
||||
let mut packet_buf = vec![0u8; packet_len];
|
||||
self.read_exact(&mut packet_buf)
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to read packet data: {e}"))?;
|
||||
// Flush to ensure data is sent immediately
|
||||
writer
|
||||
.flush()
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to flush stream: {e}"))?;
|
||||
|
||||
tracing::trace!("Received LP packet ({packet_len} bytes + 4 byte header)");
|
||||
Ok(packet_buf)
|
||||
tracing::trace!(
|
||||
"Sent LP packet ({} bytes + 4 byte header)",
|
||||
packet_data.len()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn receive_raw_packet_async_read<R>(reader: &mut R) -> std::io::Result<Vec<u8>>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
{
|
||||
// Read 4-byte length prefix (u32 big-endian)
|
||||
let mut len_buf = [0u8; 4];
|
||||
reader
|
||||
.read_exact(&mut len_buf)
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to read packet length: {e}"))?;
|
||||
|
||||
let packet_len = u32::from_be_bytes(len_buf) as usize;
|
||||
|
||||
// Sanity check to prevent huge allocations
|
||||
const MAX_PACKET_SIZE: usize = 65536; // 64KB max
|
||||
if packet_len > MAX_PACKET_SIZE {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"Packet size {packet_len} exceeds maximum {MAX_PACKET_SIZE}",
|
||||
)));
|
||||
}
|
||||
|
||||
// Read the actual packet data
|
||||
let mut packet_buf = vec![0u8; packet_len];
|
||||
reader
|
||||
.read_exact(&mut packet_buf)
|
||||
.await
|
||||
.inspect_err(|e| debug!("Failed to read packet data: {e}"))?;
|
||||
|
||||
tracing::trace!("Received LP packet ({packet_len} bytes + 4 byte header)");
|
||||
Ok(packet_buf)
|
||||
}
|
||||
|
||||
impl LpTransport for TcpStream {
|
||||
@@ -97,6 +109,14 @@ impl LpTransport for TcpStream {
|
||||
// Set TCP_NODELAY for low latency
|
||||
self.set_nodelay(nodelay)
|
||||
}
|
||||
|
||||
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()> {
|
||||
send_serialised_packet_async_write(self, packet_data).await
|
||||
}
|
||||
|
||||
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>> {
|
||||
receive_raw_packet_async_read(self).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "io-mocks")]
|
||||
@@ -108,4 +128,12 @@ impl LpTransport for MockIOStream {
|
||||
fn set_no_delay(&mut self, _nodelay: bool) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()> {
|
||||
send_serialised_packet_async_write(self, packet_data).await
|
||||
}
|
||||
|
||||
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>> {
|
||||
receive_raw_packet_async_read(self).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,12 @@ sha2 = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
# rand 0.9 for KKT integration (nym-kkt uses rand 0.9)
|
||||
rand09 = { package = "rand", version = "0.9.2" }
|
||||
rand09 = { workspace = true }
|
||||
|
||||
nym-crypto = { path = "../crypto", features = ["hashing", "asymmetric"] }
|
||||
nym-kkt = { path = "../nym-kkt" }
|
||||
nym-lp-common = { path = "../nym-lp-common" }
|
||||
nym-lp-transport = { path = "../nym-lp-transport" }
|
||||
|
||||
# libcrux dependencies for PSQ (Post-Quantum PSK derivation)
|
||||
libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = [
|
||||
@@ -34,12 +35,21 @@ num_enum = { workspace = true }
|
||||
chacha20poly1305 = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
# needed for the 'mock 'feature
|
||||
nym-test-utils = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
rand_chacha = "0.3"
|
||||
#rand_chacha = "0.3"
|
||||
mock_instant = { workspace = true }
|
||||
nym-crypto = { path = "../crypto", features = ["rand"] }
|
||||
nym-test-utils = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
nym-lp-transport = { path = "../nym-lp-transport", features = ["io-mocks"] }
|
||||
|
||||
[features]
|
||||
mock = ["nym-test-utils", "nym-crypto/rand"]
|
||||
|
||||
[[bench]]
|
||||
name = "replay_protection"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main};
|
||||
use nym_lp::replay::ReceivingKeyCounterValidator;
|
||||
use nym_test_utils::helpers::u64_seeded_rng;
|
||||
use parking_lot::Mutex;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
use rand::Rng;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn bench_sequential_counters(c: &mut Criterion) {
|
||||
@@ -47,7 +47,7 @@ fn bench_out_of_order_counters(c: &mut Criterion) {
|
||||
let validator = ReceivingKeyCounterValidator::default();
|
||||
|
||||
// Create random counters within a valid window
|
||||
let mut rng = ChaCha8Rng::seed_from_u64(42);
|
||||
let mut rng = u64_seeded_rng(42);
|
||||
let counters: Vec<u64> = (0..size).map(|_| rng.gen_range(0..1024)).collect();
|
||||
|
||||
b.iter(|| {
|
||||
|
||||
@@ -555,7 +555,7 @@ mod tests {
|
||||
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
|
||||
buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index
|
||||
buf.extend_from_slice(&123u64.to_le_bytes()); // Counter
|
||||
buf.extend_from_slice(&255u16.to_le_bytes()); // Invalid message type
|
||||
buf.extend_from_slice(&231u16.to_le_bytes()); // Invalid message type
|
||||
// Need payload and trailer to meet min_size requirement
|
||||
let payload_size = 10; // Arbitrary
|
||||
buf.extend_from_slice(&vec![0u8; payload_size]); // Some data
|
||||
@@ -565,7 +565,7 @@ mod tests {
|
||||
let result = parse_lp_packet(&buf, None);
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(LpError::InvalidMessageType(255)) => {} // Expected error
|
||||
Err(LpError::InvalidMessageType(231)) => {} // Expected error
|
||||
Err(e) => panic!("Expected InvalidMessageType error, got {:?}", e),
|
||||
Ok(_) => panic!("Expected error, but got Ok"),
|
||||
}
|
||||
@@ -628,7 +628,7 @@ mod tests {
|
||||
receiver_idx: 42,
|
||||
counter: 123,
|
||||
},
|
||||
message: LpMessage::ClientHello(hello_data.clone()),
|
||||
message: LpMessage::ClientHello(hello_data),
|
||||
trailer: [0; TRAILER_LEN],
|
||||
};
|
||||
|
||||
@@ -681,7 +681,7 @@ mod tests {
|
||||
receiver_idx: 100,
|
||||
counter: 200,
|
||||
},
|
||||
message: LpMessage::ClientHello(hello_data.clone()),
|
||||
message: LpMessage::ClientHello(hello_data),
|
||||
trailer: [55; TRAILER_LEN],
|
||||
};
|
||||
|
||||
@@ -1289,4 +1289,37 @@ mod tests {
|
||||
_ => panic!("Expected SubsessionKK1 message"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_parse_error() {
|
||||
use crate::message::ErrorPacketData;
|
||||
|
||||
let mut dst = BytesMut::new();
|
||||
|
||||
let error_data = ErrorPacketData {
|
||||
message: "this is an error".to_string(),
|
||||
};
|
||||
|
||||
let packet = LpPacket {
|
||||
header: LpHeader {
|
||||
protocol_version: 1,
|
||||
reserved: [0u8; 3],
|
||||
receiver_idx: 42,
|
||||
counter: 200,
|
||||
},
|
||||
message: LpMessage::Error(error_data.clone()),
|
||||
trailer: [0; TRAILER_LEN],
|
||||
};
|
||||
|
||||
serialize_lp_packet(&packet, &mut dst, None).unwrap();
|
||||
let decoded = parse_lp_packet(&dst, None).unwrap();
|
||||
|
||||
assert_eq!(decoded.header.receiver_idx, 42);
|
||||
match decoded.message {
|
||||
LpMessage::Error(data) => {
|
||||
assert_eq!(data.message, "this is an error");
|
||||
}
|
||||
_ => panic!("Expected Error message"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::message::MessageType;
|
||||
use crate::{noise_protocol::NoiseError, replay::ReplayError};
|
||||
use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
|
||||
use nym_kkt::ciphersuite::{HashFunction, KEM};
|
||||
use nym_kkt::error::KKTError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@@ -102,4 +104,25 @@ pub enum LpError {
|
||||
kem: KEM,
|
||||
hash_function: HashFunction,
|
||||
},
|
||||
|
||||
#[error("failed to complete KKT/PSQ handshake: {0}")]
|
||||
KKTPSQHandshake(String),
|
||||
|
||||
#[error("failed to complete the KKT exchange: {source}")]
|
||||
KKTFailure {
|
||||
#[from]
|
||||
source: KKTError,
|
||||
},
|
||||
}
|
||||
|
||||
impl LpError {
|
||||
pub fn kkt_psq_handshake(msg: impl Into<String>) -> Self {
|
||||
Self::KKTPSQHandshake(msg.into())
|
||||
}
|
||||
|
||||
pub fn unexpected_handshake_response(got: MessageType, expected: MessageType) -> LpError {
|
||||
Self::KKTPSQHandshake(format!(
|
||||
"received unexpected response, got: {got:?}, expected: {expected:?}"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
+136
-61
@@ -10,6 +10,7 @@ pub mod noise_protocol;
|
||||
pub mod packet;
|
||||
pub mod peer;
|
||||
pub mod psk;
|
||||
mod psq;
|
||||
pub mod replay;
|
||||
pub mod session;
|
||||
mod session_integration;
|
||||
@@ -21,62 +22,156 @@ pub use error::LpError;
|
||||
pub use message::{ClientHelloData, LpMessage};
|
||||
pub use packet::{BOOTSTRAP_RECEIVER_IDX, LpPacket, OuterHeader};
|
||||
pub use replay::{ReceivingKeyCounterValidator, ReplayError};
|
||||
pub use session::{LpSession, generate_fresh_salt};
|
||||
pub use session::LpSession;
|
||||
pub use session_manager::SessionManager;
|
||||
pub use state_machine::LpStateMachine;
|
||||
|
||||
pub const NOISE_PATTERN: &str = "Noise_XKpsk3_25519_ChaChaPoly_SHA256";
|
||||
pub const NOISE_PSK_INDEX: u8 = 3;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub struct SessionsMock {
|
||||
pub initiator: LpSession,
|
||||
pub responder: LpSession,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
impl SessionsMock {
|
||||
pub fn mock_post_handshake(session_id: u32) -> SessionsMock {
|
||||
use crate::peer::mock_peers;
|
||||
use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey};
|
||||
|
||||
let (init, resp) = mock_peers();
|
||||
let resp_remote = resp.as_remote();
|
||||
let init_remote = init.as_remote();
|
||||
let salt = [42u8; 32];
|
||||
let session_id_bytes = session_id.to_le_bytes();
|
||||
|
||||
// skip KKT by just deriving the kem key locally
|
||||
let kem_keys = resp.kem_psq.as_ref().unwrap();
|
||||
|
||||
let libcrux_private_key = libcrux_kem::PrivateKey::decode(
|
||||
libcrux_kem::Algorithm::X25519,
|
||||
kem_keys.private_key().as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
let decapsulation_key = DecapsulationKey::X25519(libcrux_private_key);
|
||||
|
||||
let libcrux_public_key = libcrux_kem::PublicKey::decode(
|
||||
libcrux_kem::Algorithm::X25519,
|
||||
kem_keys.public_key().as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
let encapsulation_key = EncapsulationKey::X25519(libcrux_public_key);
|
||||
|
||||
// INIT -> RESP: PSQ MSG1
|
||||
let psq_initiator = crate::psk::psq_initiator_create_message(
|
||||
init.x25519.private_key(),
|
||||
&resp_remote.x25519_public,
|
||||
&encapsulation_key,
|
||||
init.ed25519.private_key(),
|
||||
init.ed25519.public_key(),
|
||||
&salt,
|
||||
&session_id_bytes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let psk = psq_initiator.psk;
|
||||
let psq_payload = psq_initiator.payload;
|
||||
let outer_aead_key = crate::codec::OuterAeadKey::from_psk(&psk);
|
||||
|
||||
let noise_state_init = snow::Builder::new(crate::noise_protocol::NoiseProtocol::params())
|
||||
.local_private_key(init.x25519().private_key().as_bytes())
|
||||
.remote_public_key(resp_remote.x25519_public.as_bytes())
|
||||
.psk(crate::NOISE_PSK_INDEX, &psk)
|
||||
.build_initiator()
|
||||
.unwrap();
|
||||
let mut noise_protocol_init = crate::noise_protocol::NoiseProtocol::new(noise_state_init);
|
||||
let noise_msg1 = noise_protocol_init.get_bytes_to_send().unwrap().unwrap();
|
||||
|
||||
let psq_responder = crate::psk::psq_responder_process_message(
|
||||
resp.x25519.private_key(),
|
||||
&init_remote.x25519_public,
|
||||
(&decapsulation_key, &encapsulation_key),
|
||||
&init_remote.ed25519_public,
|
||||
&psq_payload,
|
||||
&salt,
|
||||
&session_id_bytes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let noise_state_resp = snow::Builder::new(crate::noise_protocol::NoiseProtocol::params())
|
||||
.local_private_key(resp.x25519().private_key().as_bytes())
|
||||
.remote_public_key(init_remote.x25519_public.as_bytes())
|
||||
.psk(crate::NOISE_PSK_INDEX, &psk)
|
||||
.build_responder()
|
||||
.unwrap();
|
||||
let mut noise_protocol_resp = crate::noise_protocol::NoiseProtocol::new(noise_state_resp);
|
||||
noise_protocol_resp.read_message(&noise_msg1).unwrap();
|
||||
|
||||
let noise_msg2 = noise_protocol_resp.get_bytes_to_send().unwrap().unwrap();
|
||||
noise_protocol_init.read_message(&noise_msg2).unwrap();
|
||||
let noise_msg3 = noise_protocol_init.get_bytes_to_send().unwrap().unwrap();
|
||||
|
||||
assert!(noise_protocol_init.is_handshake_finished());
|
||||
|
||||
noise_protocol_resp.read_message(&noise_msg3).unwrap();
|
||||
assert!(noise_protocol_resp.is_handshake_finished());
|
||||
|
||||
SessionsMock {
|
||||
initiator: LpSession::new(
|
||||
session_id,
|
||||
1,
|
||||
outer_aead_key.clone(),
|
||||
init,
|
||||
resp_remote,
|
||||
crate::session::PqSharedSecret::new(psq_initiator.pq_shared_secret),
|
||||
noise_protocol_init,
|
||||
),
|
||||
responder: LpSession::new(
|
||||
session_id,
|
||||
1,
|
||||
outer_aead_key,
|
||||
resp,
|
||||
init_remote,
|
||||
crate::session::PqSharedSecret::new(psq_responder.pq_shared_secret),
|
||||
noise_protocol_resp,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// we just need a dummy 'valid' session for simpler tests
|
||||
pub fn mock_initiator() -> LpSession {
|
||||
Self::mock_post_handshake(1234).initiator
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub fn sessions_for_tests() -> (LpSession, LpSession) {
|
||||
let (init, resp) = crate::peer::mock_peers();
|
||||
let sessions = SessionsMock::mock_post_handshake(69);
|
||||
(sessions.initiator, sessions.responder)
|
||||
}
|
||||
|
||||
// Use a fixed receiver_index for deterministic tests
|
||||
let receiver_index: u32 = 12345;
|
||||
|
||||
// Use consistent salt for deterministic tests
|
||||
let salt = [1u8; 32];
|
||||
|
||||
let initiator_session = LpSession::new(
|
||||
receiver_index,
|
||||
true,
|
||||
init.clone(),
|
||||
resp.as_remote(),
|
||||
&salt,
|
||||
packet::version::CURRENT,
|
||||
)
|
||||
.expect("Test session creation failed");
|
||||
|
||||
let responder_session = LpSession::new(
|
||||
receiver_index,
|
||||
false,
|
||||
resp,
|
||||
init.as_remote(),
|
||||
&salt,
|
||||
packet::version::CURRENT,
|
||||
)
|
||||
.expect("Test session creation failed");
|
||||
|
||||
(initiator_session, responder_session)
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub fn mock_session_for_test() -> LpSession {
|
||||
SessionsMock::mock_initiator()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::message::LpMessage;
|
||||
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN, version};
|
||||
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
|
||||
use crate::session_manager::SessionManager;
|
||||
use crate::{LpError, sessions_for_tests};
|
||||
use crate::{LpError, SessionsMock, mock_session_for_test};
|
||||
use bytes::BytesMut;
|
||||
|
||||
// Import the new standalone functions
|
||||
use crate::codec::{parse_lp_packet, serialize_lp_packet};
|
||||
use crate::peer::mock_peers;
|
||||
|
||||
#[test]
|
||||
fn test_replay_protection_integration() {
|
||||
// Create session
|
||||
let session = sessions_for_tests().0;
|
||||
let mut session = mock_session_for_test();
|
||||
|
||||
// === Packet 1 (Counter 0 - Should succeed) ===
|
||||
let packet1 = LpPacket {
|
||||
@@ -175,40 +270,20 @@ mod tests {
|
||||
#[test]
|
||||
fn test_session_manager_integration() {
|
||||
// Create session manager
|
||||
let local_manager = SessionManager::new();
|
||||
let remote_manager = SessionManager::new();
|
||||
|
||||
// Generate Ed25519 keypairs for PSQ authentication
|
||||
let (init, resp) = mock_peers();
|
||||
let mut local_manager = SessionManager::new();
|
||||
let mut remote_manager = SessionManager::new();
|
||||
|
||||
// Use fixed receiver_index for deterministic test
|
||||
let receiver_index: u32 = 54321;
|
||||
|
||||
// Test salt
|
||||
let salt = [46u8; 32];
|
||||
let sessions = SessionsMock::mock_post_handshake(receiver_index);
|
||||
let local_session = sessions.initiator;
|
||||
let remote_session = sessions.responder;
|
||||
|
||||
// Create a session via manager
|
||||
let _ = local_manager
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
true,
|
||||
init.clone(),
|
||||
resp.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.unwrap();
|
||||
let _ = local_manager.create_session_state_machine(local_session);
|
||||
let _ = remote_manager.create_session_state_machine(remote_session);
|
||||
|
||||
let _ = remote_manager
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
false,
|
||||
resp,
|
||||
init.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.unwrap();
|
||||
// === Packet 1 (Counter 0 - Should succeed) ===
|
||||
let packet1 = LpPacket {
|
||||
header: LpHeader {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// 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};
|
||||
use crate::{BOOTSTRAP_RECEIVER_IDX, LpError, LpPacket};
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
@@ -12,7 +13,7 @@ use std::fmt::{self, Display};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
|
||||
/// Data structure for the ClientHello message
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ClientHelloData {
|
||||
/// Client-proposed receiver index for session identification (4 bytes)
|
||||
/// Auto-generated randomly by the client
|
||||
@@ -29,6 +30,17 @@ impl ClientHelloData {
|
||||
// 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
|
||||
}
|
||||
@@ -142,6 +154,8 @@ pub enum MessageType {
|
||||
SubsessionReady = 0x000C,
|
||||
/// Subsession abort - race winner tells loser to become responder
|
||||
SubsessionAbort = 0x000D,
|
||||
/// General error
|
||||
Error = 0x00FF,
|
||||
}
|
||||
|
||||
impl MessageType {
|
||||
@@ -158,6 +172,9 @@ impl MessageType {
|
||||
pub struct HandshakeData(pub Vec<u8>);
|
||||
|
||||
impl HandshakeData {
|
||||
pub(crate) fn new(bytes: Vec<u8>) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
@@ -175,6 +192,11 @@ impl HandshakeData {
|
||||
pub struct EncryptedDataPayload(pub Vec<u8>);
|
||||
|
||||
impl EncryptedDataPayload {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn new(bytes: Vec<u8>) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
@@ -193,6 +215,10 @@ impl EncryptedDataPayload {
|
||||
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()
|
||||
}
|
||||
@@ -211,6 +237,10 @@ impl KKTRequestData {
|
||||
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()
|
||||
}
|
||||
@@ -224,6 +254,52 @@ impl KKTResponseData {
|
||||
}
|
||||
}
|
||||
|
||||
/// General human-readable error message
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ErrorPacketData {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl ErrorPacketData {
|
||||
pub(crate) fn new(message: impl Into<String>) -> Self {
|
||||
ErrorPacketData {
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
// length-encoding + message
|
||||
4 + self.message.len()
|
||||
}
|
||||
|
||||
fn encode(&self, dst: &mut BytesMut) {
|
||||
dst.put_u32_le(self.message.len() as u32);
|
||||
dst.put_slice(self.message.as_bytes());
|
||||
}
|
||||
|
||||
fn decode(bytes: &[u8]) -> Result<Self, LpError> {
|
||||
if bytes.len() < 4 {
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Too few bytes to deserialise ErrorPacketData. got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let message_len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
|
||||
if bytes[4..].len() != message_len {
|
||||
return Err(LpError::DeserializationError(format!(
|
||||
"Wrong number of bytes to deserialise ErrorPacketData. got {}. Expected {}",
|
||||
bytes.len(),
|
||||
4 + message_len
|
||||
)));
|
||||
}
|
||||
|
||||
let message = String::from_utf8_lossy(&bytes[4..]).to_string();
|
||||
|
||||
Ok(ErrorPacketData { message })
|
||||
}
|
||||
}
|
||||
|
||||
/// Packet forwarding request with embedded inner LP packet
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ForwardPacketData {
|
||||
@@ -449,6 +525,62 @@ pub enum LpMessage {
|
||||
SubsessionReady(SubsessionReadyData),
|
||||
/// Subsession abort - race winner tells loser to become responder (empty, signal only)
|
||||
SubsessionAbort,
|
||||
/// An error has occurred
|
||||
Error(ErrorPacketData),
|
||||
}
|
||||
|
||||
impl From<HandshakeData> for LpMessage {
|
||||
fn from(value: HandshakeData) -> Self {
|
||||
LpMessage::Handshake(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<ForwardPacketData> for LpMessage {
|
||||
fn from(value: ForwardPacketData) -> Self {
|
||||
LpMessage::ForwardPacket(value)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -468,6 +600,7 @@ impl Display for LpMessage {
|
||||
LpMessage::SubsessionKK2(_) => write!(f, "SubsessionKK2"),
|
||||
LpMessage::SubsessionReady(_) => write!(f, "SubsessionReady"),
|
||||
LpMessage::SubsessionAbort => write!(f, "SubsessionAbort"),
|
||||
LpMessage::Error(_) => write!(f, "Error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -489,6 +622,7 @@ impl LpMessage {
|
||||
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 (?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,6 +642,7 @@ impl LpMessage {
|
||||
LpMessage::SubsessionKK2(_) => false, // Always has payload
|
||||
LpMessage::SubsessionReady(_) => false, // Always has receiver_index
|
||||
LpMessage::SubsessionAbort => true, // Empty signal
|
||||
LpMessage::Error(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,6 +662,7 @@ impl LpMessage {
|
||||
LpMessage::SubsessionKK2(payload) => payload.len(),
|
||||
LpMessage::SubsessionReady(payload) => payload.len(),
|
||||
LpMessage::SubsessionAbort => 0,
|
||||
LpMessage::Error(payload) => payload.len(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,6 +682,7 @@ impl LpMessage {
|
||||
LpMessage::SubsessionKK2(_) => MessageType::SubsessionKK2,
|
||||
LpMessage::SubsessionReady(_) => MessageType::SubsessionReady,
|
||||
LpMessage::SubsessionAbort => MessageType::SubsessionAbort,
|
||||
LpMessage::Error(_) => MessageType::Error,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,6 +702,7 @@ impl LpMessage {
|
||||
LpMessage::SubsessionKK2(data) => data.encode(dst),
|
||||
LpMessage::SubsessionReady(data) => data.encode(dst),
|
||||
LpMessage::SubsessionAbort => { /* No content - signal only */ }
|
||||
LpMessage::Error(data) => data.encode(dst),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,6 +755,7 @@ impl LpMessage {
|
||||
content.ensure_empty()?;
|
||||
Ok(LpMessage::SubsessionAbort)
|
||||
}
|
||||
MessageType::Error => Ok(LpMessage::Error(ErrorPacketData::decode(content)?)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,13 @@ pub enum ReadResult {
|
||||
// --- 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`).
|
||||
@@ -91,6 +98,46 @@ impl NoiseProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -288,43 +335,3 @@ impl NoiseProtocol {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_noise_state(
|
||||
local_private_key: &[u8],
|
||||
remote_public_key: &[u8],
|
||||
psk: &[u8],
|
||||
) -> Result<NoiseProtocol, NoiseError> {
|
||||
let pattern_name = crate::NOISE_PATTERN;
|
||||
let psk_index = crate::NOISE_PSK_INDEX;
|
||||
let noise_params: NoiseParams = pattern_name.parse().unwrap();
|
||||
|
||||
let builder = snow::Builder::new(noise_params.clone());
|
||||
// Using dummy remote key as it's not needed for state creation itself
|
||||
// In a real scenario, the key would depend on initiator/responder role
|
||||
let handshake_state = builder
|
||||
.local_private_key(local_private_key)
|
||||
.remote_public_key(remote_public_key) // Use own public as dummy remote
|
||||
.psk(psk_index, psk)
|
||||
.build_initiator()?;
|
||||
Ok(NoiseProtocol::new(handshake_state))
|
||||
}
|
||||
|
||||
pub fn create_noise_state_responder(
|
||||
local_private_key: &[u8],
|
||||
remote_public_key: &[u8],
|
||||
psk: &[u8],
|
||||
) -> Result<NoiseProtocol, NoiseError> {
|
||||
let pattern_name = crate::NOISE_PATTERN;
|
||||
let psk_index = crate::NOISE_PSK_INDEX;
|
||||
let noise_params: NoiseParams = pattern_name.parse().unwrap();
|
||||
|
||||
let builder = snow::Builder::new(noise_params.clone());
|
||||
// Using dummy remote key as it's not needed for state creation itself
|
||||
// In a real scenario, the key would depend on initiator/responder role
|
||||
let handshake_state = builder
|
||||
.local_private_key(local_private_key)
|
||||
.remote_public_key(remote_public_key) // Use own public as dummy remote
|
||||
.psk(psk_index, psk)
|
||||
.build_responder()?;
|
||||
Ok(NoiseProtocol::new(handshake_state))
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::LpError;
|
||||
use crate::message::LpMessage;
|
||||
use crate::message::{LpMessage, MessageType};
|
||||
use crate::replay::ReceivingKeyCounterValidator;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use nym_lp_common::format_debug_bytes;
|
||||
@@ -53,6 +53,10 @@ impl LpPacket {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::ClientHelloData;
|
||||
use crate::{ClientHelloData, LpError};
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_kkt::ciphersuite::{KEM, KEMKeyDigests, SignatureScheme, SigningKeyDigests};
|
||||
use nym_kkt::ciphersuite::{Ciphersuite, KEM, KEMKeyDigests, SignatureScheme, SigningKeyDigests};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -52,6 +52,14 @@ 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 {
|
||||
@@ -137,16 +145,37 @@ impl LpRemotePeer {
|
||||
self.expected_signing_key_digests = expected_signing_key_digests;
|
||||
self
|
||||
}
|
||||
|
||||
/// Attempt to retrieve expected KEM key hash of the remote
|
||||
/// for [`nym_kkt::ciphersuite::KEM`] key type and [`nym_kkt::ciphersuite::HashFunction`]
|
||||
/// specified by own [`nym_kkt::ciphersuite::Ciphersuite`]
|
||||
pub(crate) fn expected_kem_key_hash(
|
||||
&self,
|
||||
ciphersuite: Ciphersuite,
|
||||
) -> Result<Vec<u8>, LpError> {
|
||||
let kem = ciphersuite.kem();
|
||||
let hash_function = ciphersuite.hash_function();
|
||||
|
||||
let digests = self
|
||||
.expected_kem_key_digests
|
||||
.get(&kem)
|
||||
.ok_or(LpError::NoKnownKEMKeyDigests { kem, hash_function })?;
|
||||
|
||||
digests
|
||||
.get(&hash_function)
|
||||
.ok_or(LpError::NoKnownKEMKeyDigests { kem, hash_function })
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub fn mock_peer() -> LpLocalPeer {
|
||||
// use deterministic rng
|
||||
let mut rng = nym_test_utils::helpers::deterministic_rng();
|
||||
random_peer(&mut rng)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub fn random_peer<R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> LpLocalPeer {
|
||||
let ed25519 = Arc::new(ed25519::KeyPair::new(rng));
|
||||
let x25519 = Arc::new(ed25519.to_x25519());
|
||||
@@ -159,7 +188,7 @@ pub fn random_peer<R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> LpLocalPe
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(feature = "mock", test))]
|
||||
pub fn mock_peers() -> (LpLocalPeer, LpLocalPeer) {
|
||||
// use deterministic rng
|
||||
let mut rng = nym_test_utils::helpers::deterministic_rng();
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// 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;
|
||||
|
||||
#[cfg(test)]
|
||||
use mock_instant::thread_local::{SystemTime, UNIX_EPOCH};
|
||||
#[cfg(not(test))]
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
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 {}
|
||||
@@ -0,0 +1,391 @@
|
||||
// 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::LpRemotePeer;
|
||||
use crate::psk::psq_initiator_create_message;
|
||||
use crate::psq::helpers::{LpTransportHandshakeExt, current_timestamp};
|
||||
use crate::psq::{IntermediateHandshakeFailure, PSQHandshakeState};
|
||||
use crate::session::PqSharedSecret;
|
||||
use crate::{ClientHelloData, LpError, LpMessage, LpSession};
|
||||
use nym_kkt::KKT_RESPONSE_AAD;
|
||||
use nym_kkt::ciphersuite::EncapsulationKey;
|
||||
use nym_kkt::context::KKTContext;
|
||||
use nym_kkt::encryption::{KKTSessionSecret, decrypt_kkt_frame, encrypt_initial_kkt_frame};
|
||||
use nym_kkt::session::{anonymous_initiator_process, initiator_ingest_response};
|
||||
use nym_lp_transport::traits::LpTransport;
|
||||
use rand09::rng;
|
||||
use tracing::debug;
|
||||
|
||||
impl<'a, S> PSQHandshakeState<'a, S>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
/// Generate and send client hello to the responder
|
||||
pub(crate) async fn send_client_hello(&mut self) -> Result<ClientHelloData, LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
// 1. Generate and send ClientHelloData with fresh salt and both public keys
|
||||
let timestamp = current_timestamp()?;
|
||||
|
||||
let client_hello_data = self.local_peer.build_client_hello_data(timestamp);
|
||||
self.connection
|
||||
.send_packet(client_hello_data.into_lp_packet(protocol), None)
|
||||
.await?;
|
||||
Ok(client_hello_data)
|
||||
}
|
||||
|
||||
/// Attempt to receive an ack to sent client hello. returns a boolean indicating
|
||||
/// whether the request has been successful or whether there has been a collision in receiver
|
||||
/// index requiring a retry
|
||||
pub(crate) async fn receive_client_hello_ack(&mut self) -> Result<bool, LpError> {
|
||||
match self.receive_non_error(None).await?.message {
|
||||
LpMessage::Ack => Ok(true),
|
||||
LpMessage::Collision => Ok(false),
|
||||
other => {
|
||||
// TODO: retry on collision
|
||||
Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::Ack,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to send KKT request to begin the handshake
|
||||
pub(crate) async fn send_kkt_request(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
remote_peer: &LpRemotePeer,
|
||||
) -> Result<(KKTContext, KKTSessionSecret), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let (kkt_context, kkt_frame) = anonymous_initiator_process(&mut rng(), self.ciphersuite)?;
|
||||
let (session_secret, encrypted_frame) =
|
||||
encrypt_initial_kkt_frame(&mut rng(), &remote_peer.x25519_public, &kkt_frame)?;
|
||||
let lp_message = KKTRequestData::new(encrypted_frame).into();
|
||||
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||
self.connection.send_packet(lp_packet, None).await?;
|
||||
Ok((kkt_context, session_secret))
|
||||
}
|
||||
|
||||
/// Attempt to receive a KKT response to the previously sent request and extract (and validate)
|
||||
/// the received encapsulation key
|
||||
pub(crate) async fn receive_kkt_response(
|
||||
&mut self,
|
||||
(kkt_context, session_secret): (KKTContext, KKTSessionSecret),
|
||||
remote_peer: &LpRemotePeer,
|
||||
) -> Result<EncapsulationKey<'static>, LpError> {
|
||||
let kkt_response = match self.receive_non_error(None).await?.message {
|
||||
LpMessage::KKTResponse(response) => response,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::KKTResponse,
|
||||
));
|
||||
}
|
||||
};
|
||||
debug!("received KKT response");
|
||||
let expected_kem_key_digest = remote_peer.expected_kem_key_hash(self.ciphersuite)?;
|
||||
|
||||
let (response_frame, remote_context) =
|
||||
decrypt_kkt_frame(&session_secret, &kkt_response.0, KKT_RESPONSE_AAD)?;
|
||||
let encapsulation_key = initiator_ingest_response(
|
||||
&kkt_context,
|
||||
&response_frame,
|
||||
&remote_context,
|
||||
&remote_peer.ed25519_public,
|
||||
&expected_kem_key_digest,
|
||||
)?;
|
||||
Ok(encapsulation_key)
|
||||
}
|
||||
|
||||
/// Attempt to prepare and send initial PSQ msg1
|
||||
pub(crate) async fn send_psq_initiator_message(
|
||||
&mut self,
|
||||
remote_peer: &LpRemotePeer,
|
||||
encapsulation_key: &EncapsulationKey<'_>,
|
||||
salt: &[u8; 32],
|
||||
session_id_bytes: &[u8; 4],
|
||||
) -> Result<(OuterAeadKey, NoiseProtocol, PqSharedSecret), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
let session_id = u32::from_le_bytes(*session_id_bytes);
|
||||
|
||||
let psq_initiator = psq_initiator_create_message(
|
||||
self.local_peer.x25519.private_key(),
|
||||
&remote_peer.x25519_public,
|
||||
encapsulation_key,
|
||||
self.local_peer.ed25519.private_key(),
|
||||
self.local_peer.ed25519.public_key(),
|
||||
salt,
|
||||
session_id_bytes,
|
||||
)?;
|
||||
let psk = psq_initiator.psk;
|
||||
let psq_payload = psq_initiator.payload;
|
||||
|
||||
// TEMP \/
|
||||
let outer_aead_key = OuterAeadKey::from_psk(&psk);
|
||||
// TEMP /\
|
||||
|
||||
// prepare noise state and msg1
|
||||
let mut noise_protocol = NoiseProtocol::build_new_initiator(
|
||||
self.local_peer.x25519().private_key().as_bytes(),
|
||||
remote_peer.x25519_public.as_bytes(),
|
||||
&psk,
|
||||
)?;
|
||||
|
||||
// prepare noise msg1
|
||||
let noise_msg1 = noise_protocol
|
||||
.get_bytes_to_send()
|
||||
.ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg1"))??;
|
||||
let psq_len = psq_payload.len() as u16;
|
||||
let mut combined = Vec::with_capacity(2 + psq_payload.len() + noise_msg1.len());
|
||||
combined.extend_from_slice(&psq_len.to_le_bytes());
|
||||
combined.extend_from_slice(&psq_payload);
|
||||
combined.extend_from_slice(&noise_msg1);
|
||||
|
||||
let lp_message = HandshakeData::new(combined).into();
|
||||
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||
|
||||
self.connection.send_packet(lp_packet, None).await?;
|
||||
Ok((
|
||||
outer_aead_key,
|
||||
noise_protocol,
|
||||
PqSharedSecret::new(psq_initiator.pq_shared_secret),
|
||||
))
|
||||
}
|
||||
|
||||
/// Attempt to receive and validate received PSQ msg2
|
||||
pub(crate) async fn receive_psq_responder_message(
|
||||
&mut self,
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
noise_protocol: &mut NoiseProtocol,
|
||||
) -> Result<(), LpError> {
|
||||
let psq_msg2 = match self
|
||||
.connection
|
||||
.receive_packet(Some(outer_aead_key))
|
||||
.await?
|
||||
.message
|
||||
{
|
||||
LpMessage::Handshake(response) => response.0,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::Handshake,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Extract PSK handle: [u16 handle_len][handle_bytes][noise_msg]
|
||||
if psq_msg2.len() < 2 {
|
||||
return Err(LpError::kkt_psq_handshake("too short msg2 received"));
|
||||
}
|
||||
let handle_len = u16::from_le_bytes([psq_msg2[0], psq_msg2[1]]) as usize;
|
||||
if psq_msg2.len() < 2 + handle_len {
|
||||
return Err(LpError::kkt_psq_handshake("too short msg2 received"));
|
||||
}
|
||||
// Extract and "store" the PSK handle
|
||||
let _psq_handle_bytes = &psq_msg2[2..2 + handle_len];
|
||||
let noise_payload = &psq_msg2[2 + handle_len..];
|
||||
|
||||
// *sigh* ignore the message
|
||||
let _noise_msg2 = noise_protocol.read_message(noise_payload)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to prepare and send final PSQ msg3
|
||||
pub(crate) async fn send_final_psq_message(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
noise_protocol: &mut NoiseProtocol,
|
||||
) -> Result<(), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let noise_msg3 = noise_protocol
|
||||
.get_bytes_to_send()
|
||||
.ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg3"))??;
|
||||
|
||||
let lp_message = HandshakeData::new(noise_msg3).into();
|
||||
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||
self.connection
|
||||
.send_packet(lp_packet, Some(outer_aead_key))
|
||||
.await?;
|
||||
|
||||
if !noise_protocol.is_handshake_finished() {
|
||||
return Err(LpError::kkt_psq_handshake(
|
||||
"noise handshake not finished after msg3",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receive final ACK that indicates finalisation of the handshake
|
||||
pub(crate) async fn receive_final_ack(
|
||||
&mut self,
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
) -> Result<(), LpError> {
|
||||
match self
|
||||
.connection
|
||||
.receive_packet(Some(outer_aead_key))
|
||||
.await?
|
||||
.message
|
||||
{
|
||||
LpMessage::Ack => Ok(()),
|
||||
other => Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::Ack,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn complete_as_initiator_inner(
|
||||
&mut self,
|
||||
) -> Result<LpSession, IntermediateHandshakeFailure>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
// 0. retrieve the expected kem key hash. if we don't know it,
|
||||
// there's no point in even trying to start the handshake
|
||||
let Some(remote_peer) = self.remote_peer.take() else {
|
||||
return Err(IntermediateHandshakeFailure::plain(
|
||||
LpError::kkt_psq_handshake("initiator can't proceed without remote information"),
|
||||
));
|
||||
};
|
||||
|
||||
// 1. Generate and send ClientHelloData with fresh salt and both public keys
|
||||
// and keep retrying until we manage to establish a receiver index without collisions
|
||||
let mut attempt = 0;
|
||||
let client_hello_data = loop {
|
||||
attempt += 1;
|
||||
|
||||
debug!("sending client hello");
|
||||
let client_hello = self
|
||||
.send_client_hello()
|
||||
.await
|
||||
.map_err(IntermediateHandshakeFailure::plain)?;
|
||||
if self
|
||||
.receive_client_hello_ack()
|
||||
.await
|
||||
.map_err(IntermediateHandshakeFailure::plain)?
|
||||
{
|
||||
debug!("received client hello ACK");
|
||||
break client_hello;
|
||||
}
|
||||
debug!("received client hello collision");
|
||||
|
||||
// TODO: make it configurable
|
||||
if attempt > 3 {
|
||||
return Err(IntermediateHandshakeFailure::plain(
|
||||
LpError::kkt_psq_handshake(
|
||||
"failed to establish receiver index without collision",
|
||||
),
|
||||
));
|
||||
}
|
||||
};
|
||||
let session_id = client_hello_data.receiver_index;
|
||||
let session_id_bytes = session_id.to_le_bytes();
|
||||
let salt = client_hello_data.salt;
|
||||
|
||||
// 3. prepare and send KKT request
|
||||
debug!("sending KKT request");
|
||||
let kkt_data = self
|
||||
.send_kkt_request(session_id, &remote_peer)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
|
||||
// 4. receive and process KKT response
|
||||
let encapsulation_key = self
|
||||
.receive_kkt_response(kkt_data, &remote_peer)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
debug!("received KKT response");
|
||||
|
||||
// 5. prepare and send PSQ msg1
|
||||
debug!("sending PSQ msg1");
|
||||
let (outer_aead_key, mut noise_protocol, pq_shared_secret) = self
|
||||
.send_psq_initiator_message(&remote_peer, &encapsulation_key, &salt, &session_id_bytes)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
|
||||
// 6. receive and process PSQ msg2
|
||||
debug!("received PSQ msg2");
|
||||
if let Err(source) = self
|
||||
.receive_psq_responder_message(&outer_aead_key, &mut noise_protocol)
|
||||
.await
|
||||
{
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
// 7. prepare and send PSQ msg3
|
||||
debug!("sending PSQ msg3");
|
||||
if let Err(source) = self
|
||||
.send_final_psq_message(session_id, &outer_aead_key, &mut noise_protocol)
|
||||
.await
|
||||
{
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
// 8. receive final ACK and finalise
|
||||
debug!("received final ACK");
|
||||
if let Err(source) = self.receive_final_ack(&outer_aead_key).await {
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
Ok(LpSession::new(
|
||||
session_id,
|
||||
self.protocol_version()
|
||||
.expect("protocol version is known at this point"),
|
||||
outer_aead_key,
|
||||
self.local_peer.clone(),
|
||||
remote_peer,
|
||||
pq_shared_secret,
|
||||
noise_protocol,
|
||||
))
|
||||
}
|
||||
|
||||
// TODO: missing: receive counter check
|
||||
pub async fn complete_as_initiator(mut self) -> Result<LpSession, LpError>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
match self.complete_as_initiator_inner().await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => Err(self.try_send_error_packet(err).await),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::codec::OuterAeadKey;
|
||||
use crate::message::ErrorPacketData;
|
||||
use crate::packet::LpHeader;
|
||||
use crate::peer::{LpLocalPeer, LpRemotePeer};
|
||||
use crate::psq::helpers::LpTransportHandshakeExt;
|
||||
use crate::{LpError, LpMessage, LpPacket};
|
||||
use nym_kkt::ciphersuite::Ciphersuite;
|
||||
use nym_lp_transport::traits::LpTransport;
|
||||
use tracing::debug;
|
||||
|
||||
mod helpers;
|
||||
mod initiator;
|
||||
mod responder;
|
||||
|
||||
pub(crate) struct IntermediateHandshakeFailure {
|
||||
/// Session id established during exchange if we managed to derive it
|
||||
session_id: Option<u32>,
|
||||
|
||||
/// Protocol version established during the exchange
|
||||
protocol_version: Option<u8>,
|
||||
|
||||
/// Outer aead key established during exchange if we managed to derive it
|
||||
outer_aead_key: Option<OuterAeadKey>,
|
||||
|
||||
/// The error source
|
||||
source: LpError,
|
||||
}
|
||||
|
||||
impl IntermediateHandshakeFailure {
|
||||
fn plain(source: LpError) -> IntermediateHandshakeFailure {
|
||||
IntermediateHandshakeFailure {
|
||||
session_id: None,
|
||||
protocol_version: None,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 client hello (responder)
|
||||
protocol_version: Option<u8>,
|
||||
|
||||
/// Ciphersuite selected for the KKT/PSQ exchange
|
||||
ciphersuite: Ciphersuite,
|
||||
|
||||
/// 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: Option<LpRemotePeer>,
|
||||
|
||||
/// Counter for outgoing packets
|
||||
sending_counter: u64,
|
||||
}
|
||||
|
||||
impl<'a, S> PSQHandshakeState<'a, S>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
pub fn new(connection: &'a mut S, ciphersuite: Ciphersuite, local_peer: LpLocalPeer) -> Self {
|
||||
PSQHandshakeState {
|
||||
connection,
|
||||
protocol_version: None,
|
||||
ciphersuite,
|
||||
local_peer,
|
||||
remote_peer: None,
|
||||
sending_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_protocol_version(mut self, protocol_version: u8) -> Self {
|
||||
self.protocol_version = Some(protocol_version);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_remote_peer(mut self, remote_peer: LpRemotePeer) -> Self {
|
||||
self.remote_peer = Some(remote_peer);
|
||||
self
|
||||
}
|
||||
|
||||
fn protocol_version(&self) -> Result<u8, LpError> {
|
||||
self.protocol_version
|
||||
.ok_or_else(|| LpError::kkt_psq_handshake("unknown protocol version"))
|
||||
}
|
||||
|
||||
/// Generates the next counter value for outgoing packets.
|
||||
pub fn next_counter(&mut self) -> u64 {
|
||||
let counter = self.sending_counter;
|
||||
self.sending_counter += 1;
|
||||
counter
|
||||
}
|
||||
|
||||
pub fn next_packet(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
protocol_version: u8,
|
||||
message: LpMessage,
|
||||
) -> LpPacket {
|
||||
let counter = self.next_counter();
|
||||
let header = LpHeader::new(session_id, counter, protocol_version);
|
||||
LpPacket::new(header, message)
|
||||
}
|
||||
|
||||
pub(crate) async fn try_send_error_packet(
|
||||
&mut self,
|
||||
err: IntermediateHandshakeFailure,
|
||||
) -> LpError {
|
||||
// if session_id is not known, we can't send the packet back (with the current design)
|
||||
let (Some(session_id), Some(protocol)) = (err.session_id, err.protocol_version) else {
|
||||
return err.source;
|
||||
};
|
||||
if let Err(err) = self
|
||||
.send_error_packet(
|
||||
session_id,
|
||||
protocol,
|
||||
err.source.to_string(),
|
||||
err.outer_aead_key.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
debug!("failed to send back error response: {err}")
|
||||
}
|
||||
err.source
|
||||
}
|
||||
|
||||
/// Attempt to send an error packet
|
||||
pub(crate) async fn send_error_packet(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
protocol_version: u8,
|
||||
msg: impl Into<String>,
|
||||
outer_aead_key: Option<&OuterAeadKey>,
|
||||
) -> Result<(), LpError> {
|
||||
let packet = self.next_packet(
|
||||
session_id,
|
||||
protocol_version,
|
||||
LpMessage::Error(ErrorPacketData::new(msg)),
|
||||
);
|
||||
self.connection.send_packet(packet, outer_aead_key).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to receive a packet from connection, explicitly checking for an error response
|
||||
/// and returning corresponding message if received
|
||||
pub(crate) async fn receive_non_error(
|
||||
&mut self,
|
||||
outer_aead_key: Option<&OuterAeadKey>,
|
||||
) -> Result<LpPacket, LpError> {
|
||||
let packet = self.connection.receive_packet(outer_aead_key).await?;
|
||||
|
||||
match &packet.message {
|
||||
LpMessage::Error(error_packet) => Err(LpError::kkt_psq_handshake(format!(
|
||||
"remote error: {}",
|
||||
error_packet.message
|
||||
))),
|
||||
_ => Ok(packet),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::peer::mock_peers;
|
||||
use crate::psq::helpers::LpTransportHandshakeExt;
|
||||
use crate::psq::responder::DEFAULT_TIMESTAMP_TOLERANCE;
|
||||
use mock_instant::thread_local::MockClock;
|
||||
use nym_kkt::ciphersuite::{HashFunction, HashLength, KEM, SignatureScheme};
|
||||
use nym_test_utils::mocks::async_read_write::MockIOStream;
|
||||
use nym_test_utils::traits::{Leak, TimeboxedSpawnable};
|
||||
use std::time::Duration;
|
||||
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();
|
||||
let conn_resp = conn_init.try_get_remote_handle();
|
||||
|
||||
// leak the connections (JUST FOR THE PURPOSE OF THIS TEST!)
|
||||
// so they'd get 'static lifetime
|
||||
let conn_init = conn_init.leak();
|
||||
let conn_resp = conn_resp.leak();
|
||||
|
||||
let ciphersuite = Ciphersuite::new(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
HashLength::Default,
|
||||
);
|
||||
|
||||
let (init, resp) = mock_peers();
|
||||
let resp_remote = resp.as_remote();
|
||||
|
||||
let handshake_init = PSQHandshakeState::new(conn_init, ciphersuite, init)
|
||||
.with_protocol_version(1)
|
||||
.with_remote_peer(resp_remote);
|
||||
let handshake_resp = PSQHandshakeState::new(conn_resp, ciphersuite, resp);
|
||||
|
||||
let resp_fut = handshake_resp.complete_as_responder().spawn_timeboxed();
|
||||
let init_fut = handshake_init.complete_as_initiator().spawn_timeboxed();
|
||||
|
||||
let (session_init, session_resp) = join!(init_fut, resp_fut);
|
||||
|
||||
let session_init = session_init???;
|
||||
let session_resp = session_resp???;
|
||||
|
||||
assert_eq!(session_init.id(), session_resp.id());
|
||||
assert_eq!(
|
||||
session_init.outer_aead_key().as_bytes(),
|
||||
session_resp.outer_aead_key().as_bytes()
|
||||
);
|
||||
assert_eq!(
|
||||
session_init.pq_shared_secret().as_bytes(),
|
||||
session_resp.pq_shared_secret().as_bytes()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn preparing_client_hello_initiator() -> anyhow::Result<()> {
|
||||
let mut conn_init = MockIOStream::default();
|
||||
let mut conn_resp = conn_init.try_get_remote_handle();
|
||||
|
||||
let ciphersuite = Ciphersuite::new(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
HashLength::Default,
|
||||
);
|
||||
let (init, resp) = mock_peers();
|
||||
let resp_remote = resp.as_remote();
|
||||
|
||||
// as initiator
|
||||
let mut handshake_init = PSQHandshakeState::new(&mut conn_init, ciphersuite, init)
|
||||
.with_protocol_version(1)
|
||||
.with_remote_peer(resp_remote);
|
||||
|
||||
// you can generate and send (valid) client hello as initiator
|
||||
let client_hello = handshake_init.send_client_hello().await?;
|
||||
let LpMessage::ClientHello(received_client_hello) =
|
||||
conn_resp.receive_packet(None).await?.message
|
||||
else {
|
||||
panic!("wrong message type");
|
||||
};
|
||||
assert_eq!(client_hello, received_client_hello);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// essentially make sure you can't accidentally trigger the handshake as the responder
|
||||
#[tokio::test]
|
||||
async fn preparing_client_hello_responder() -> anyhow::Result<()> {
|
||||
let conn_init = MockIOStream::default();
|
||||
let mut conn_resp = conn_init.try_get_remote_handle();
|
||||
|
||||
let ciphersuite = Ciphersuite::new(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
HashLength::Default,
|
||||
);
|
||||
let (_, resp) = mock_peers();
|
||||
|
||||
// as initiator
|
||||
let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp);
|
||||
|
||||
// you can generate and send (valid) client hello as initiator
|
||||
let sending_res = handshake_resp.send_client_hello().await;
|
||||
assert!(sending_res.is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_receive_client_hello_timestamp_too_skewed() -> anyhow::Result<()> {
|
||||
let current_time = Duration::from_secs(10000);
|
||||
MockClock::set_system_time(current_time);
|
||||
|
||||
let too_old = current_time - DEFAULT_TIMESTAMP_TOLERANCE - Duration::from_secs(1);
|
||||
let too_recent = current_time + DEFAULT_TIMESTAMP_TOLERANCE + Duration::from_secs(1);
|
||||
|
||||
let ciphersuite = Ciphersuite::new(
|
||||
KEM::X25519,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
HashLength::Default,
|
||||
);
|
||||
|
||||
// TOO OLD
|
||||
let mut conn_init = MockIOStream::default();
|
||||
let mut conn_resp = conn_init.try_get_remote_handle();
|
||||
let (init, resp) = mock_peers();
|
||||
|
||||
let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp);
|
||||
let client_hello_too_old = init.build_client_hello_data(too_old.as_secs());
|
||||
|
||||
conn_init
|
||||
.send_packet(client_hello_too_old.into_lp_packet(1), None)
|
||||
.await?;
|
||||
let err = handshake_resp.receive_client_hello().await.unwrap_err();
|
||||
assert!(err.to_string().contains("too old"));
|
||||
|
||||
// TOO RECENT
|
||||
let mut conn_init = MockIOStream::default();
|
||||
let mut conn_resp = conn_init.try_get_remote_handle();
|
||||
let (init, resp) = mock_peers();
|
||||
|
||||
let mut handshake_resp = PSQHandshakeState::new(&mut conn_resp, ciphersuite, resp);
|
||||
let client_hello_too_recent = init.build_client_hello_data(too_recent.as_secs());
|
||||
|
||||
conn_init
|
||||
.send_packet(client_hello_too_recent.into_lp_packet(1), None)
|
||||
.await?;
|
||||
let err = handshake_resp.receive_client_hello().await.unwrap_err();
|
||||
|
||||
assert!(err.to_string().contains("too future"));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::codec::OuterAeadKey;
|
||||
use crate::message::{HandshakeData, KKTResponseData, MessageType};
|
||||
use crate::noise_protocol::NoiseProtocol;
|
||||
use crate::peer::LpRemotePeer;
|
||||
use crate::psk::psq_responder_process_message;
|
||||
use crate::psq::helpers::{LpTransportHandshakeExt, current_timestamp};
|
||||
use crate::psq::{IntermediateHandshakeFailure, PSQHandshakeState};
|
||||
use crate::session::PqSharedSecret;
|
||||
use crate::{ClientHelloData, LpError, LpMessage, LpSession};
|
||||
use nym_kkt::KKT_RESPONSE_AAD;
|
||||
use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey};
|
||||
use nym_kkt::context::KKTContext;
|
||||
use nym_kkt::encryption::{KKTSessionSecret, decrypt_initial_kkt_frame, encrypt_kkt_frame};
|
||||
use nym_kkt::frame::KKTSessionId;
|
||||
use nym_kkt::session::{responder_ingest_message, responder_process};
|
||||
use nym_lp_transport::traits::LpTransport;
|
||||
use rand09::rng;
|
||||
use std::time::Duration;
|
||||
use tracing::debug;
|
||||
|
||||
pub const DEFAULT_TIMESTAMP_TOLERANCE: Duration = Duration::from_secs(30);
|
||||
|
||||
// this will be removed anyway, so no point in doing anything more than a hardcoded placeholder
|
||||
fn validate_client_hello_timestamp(
|
||||
client_timestamp: u64,
|
||||
tolerance: Duration,
|
||||
) -> Result<(), LpError> {
|
||||
let now = current_timestamp()?;
|
||||
|
||||
let age = now.abs_diff(client_timestamp);
|
||||
if age > tolerance.as_secs() {
|
||||
let direction = if now >= client_timestamp {
|
||||
"old"
|
||||
} else {
|
||||
"future"
|
||||
};
|
||||
|
||||
return Err(LpError::kkt_psq_handshake(format!(
|
||||
"ClientHello timestamp is too {direction} (age: {age}s, tolerance: {}s)",
|
||||
tolerance.as_secs()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl<'a, S> PSQHandshakeState<'a, S>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
pub(crate) fn encapsulated_kem_keys(
|
||||
&self,
|
||||
) -> Result<(DecapsulationKey<'static>, EncapsulationKey<'static>), LpError> {
|
||||
let kem_keys = self
|
||||
.local_peer
|
||||
.kem_psq
|
||||
.as_ref()
|
||||
.ok_or(LpError::ResponderWithMissingKEMKey)?;
|
||||
|
||||
let libcrux_private_key = libcrux_kem::PrivateKey::decode(
|
||||
libcrux_kem::Algorithm::X25519,
|
||||
kem_keys.private_key().as_bytes(),
|
||||
)
|
||||
.map_err(|e| {
|
||||
LpError::KKTError(format!(
|
||||
"Failed to convert X25519 private key to libcrux PrivateKey: {e:?}",
|
||||
))
|
||||
})?;
|
||||
let dec_key = DecapsulationKey::X25519(libcrux_private_key);
|
||||
|
||||
let libcrux_public_key = libcrux_kem::PublicKey::decode(
|
||||
libcrux_kem::Algorithm::X25519,
|
||||
kem_keys.public_key().as_bytes(),
|
||||
)
|
||||
.map_err(|e| {
|
||||
LpError::KKTError(format!(
|
||||
"Failed to convert X25519 public key to libcrux PublicKey: {e:?}",
|
||||
))
|
||||
})?;
|
||||
let enc_key = EncapsulationKey::X25519(libcrux_public_key);
|
||||
Ok((dec_key, enc_key))
|
||||
}
|
||||
|
||||
/// Attempt to receive and validate ClientHello
|
||||
pub(crate) async fn receive_client_hello(
|
||||
&mut self,
|
||||
) -> Result<(ClientHelloData, LpRemotePeer), LpError> {
|
||||
let client_hello_packet = self.receive_non_error(None).await?;
|
||||
let client_hello = match client_hello_packet.message {
|
||||
LpMessage::ClientHello(client_hello) => client_hello,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::ClientHello,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
validate_client_hello_timestamp(
|
||||
client_hello.extract_timestamp(),
|
||||
DEFAULT_TIMESTAMP_TOLERANCE,
|
||||
)?;
|
||||
|
||||
// TODO: somehow check for collision
|
||||
|
||||
// set version and remote peer information
|
||||
self.protocol_version = Some(client_hello_packet.header.protocol_version);
|
||||
let remote_peer = LpRemotePeer::new(
|
||||
client_hello.client_ed25519_public_key,
|
||||
client_hello.client_lp_public_key,
|
||||
);
|
||||
|
||||
Ok((client_hello, remote_peer))
|
||||
}
|
||||
|
||||
/// Send client hello ACK
|
||||
pub(crate) async fn send_client_hello_ack(&mut self, session_id: u32) -> Result<(), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let ack = self.next_packet(session_id, protocol, LpMessage::Ack);
|
||||
self.connection.send_packet(ack, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to receive and process a KKT request
|
||||
pub(crate) async fn receive_kkt_request(
|
||||
&mut self,
|
||||
) -> Result<(KKTContext, KKTSessionSecret, KKTSessionId), LpError> {
|
||||
let kkt_request = match self.receive_non_error(None).await?.message {
|
||||
LpMessage::KKTRequest(request) => request.0,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::KKTRequest,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let (session_secret, request_frame, remote_context) =
|
||||
decrypt_initial_kkt_frame(self.local_peer.x25519.private_key(), &kkt_request)?;
|
||||
let (context, _) = responder_ingest_message(&remote_context, None, None, &request_frame)?;
|
||||
|
||||
Ok((context, session_secret, request_frame.session_id()))
|
||||
}
|
||||
|
||||
/// Attempt to send KKT response to the previously received request
|
||||
pub(crate) async fn send_kkt_response(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
(kkt_context, session_secret, kkt_session_id): (KKTContext, KKTSessionSecret, KKTSessionId),
|
||||
encapsulation_key: &EncapsulationKey<'_>,
|
||||
) -> Result<(), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let response_frame = responder_process(
|
||||
&kkt_context,
|
||||
kkt_session_id,
|
||||
self.local_peer.ed25519().private_key(),
|
||||
encapsulation_key,
|
||||
)?;
|
||||
let encrypted_frame = encrypt_kkt_frame(
|
||||
&mut rng(),
|
||||
&session_secret,
|
||||
&response_frame,
|
||||
KKT_RESPONSE_AAD,
|
||||
)?;
|
||||
let lp_message = KKTResponseData::new(encrypted_frame).into();
|
||||
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||
|
||||
self.connection.send_packet(lp_packet, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to receive and process a PSQ msg1 request
|
||||
pub(crate) async fn receive_psq_initiator_message(
|
||||
&mut self,
|
||||
remote_peer: &LpRemotePeer,
|
||||
local_kem_keypair: (&DecapsulationKey<'_>, &EncapsulationKey<'_>),
|
||||
salt: &[u8; 32],
|
||||
session_id_bytes: &[u8; 4],
|
||||
) -> Result<(OuterAeadKey, NoiseProtocol, PqSharedSecret, Vec<u8>), LpError> {
|
||||
let psq_msg1 = match self.receive_non_error(None).await?.message {
|
||||
LpMessage::Handshake(response) => response.0,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::Handshake,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Extract PSQ payload: [u16 psq_len][psq_payload][noise_msg]
|
||||
if psq_msg1.len() < 2 {
|
||||
return Err(LpError::kkt_psq_handshake("too short msg1 received"));
|
||||
}
|
||||
let handle_len = u16::from_le_bytes([psq_msg1[0], psq_msg1[1]]) as usize;
|
||||
if psq_msg1.len() < 2 + handle_len {
|
||||
return Err(LpError::kkt_psq_handshake("too short msg1 received"));
|
||||
}
|
||||
let psq_payload = &psq_msg1[2..2 + handle_len];
|
||||
let noise_payload = &psq_msg1[2 + handle_len..];
|
||||
|
||||
// Decapsulate PSK from PSQ payload using X25519 as DHKEM
|
||||
let psq_responder = psq_responder_process_message(
|
||||
self.local_peer.x25519.private_key(),
|
||||
&remote_peer.x25519_public,
|
||||
local_kem_keypair,
|
||||
&remote_peer.ed25519_public,
|
||||
psq_payload,
|
||||
salt,
|
||||
session_id_bytes,
|
||||
)?;
|
||||
|
||||
let psk = psq_responder.psk;
|
||||
let psk_handle = psq_responder.psk_handle;
|
||||
|
||||
// TEMP \/
|
||||
let outer_aead_key = OuterAeadKey::from_psk(&psk);
|
||||
// TEMP /\
|
||||
|
||||
let mut noise_protocol = NoiseProtocol::build_new_responder(
|
||||
self.local_peer.x25519().private_key().as_bytes(),
|
||||
remote_peer.x25519_public.as_bytes(),
|
||||
&psk,
|
||||
)?;
|
||||
noise_protocol.read_message(noise_payload)?;
|
||||
|
||||
Ok((
|
||||
outer_aead_key,
|
||||
noise_protocol,
|
||||
PqSharedSecret::new(psq_responder.pq_shared_secret),
|
||||
psk_handle,
|
||||
))
|
||||
}
|
||||
|
||||
/// Attempt to prepare and generate a responder PSQ msg2
|
||||
pub(crate) async fn send_psq_responder_message(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
psk_handle: &[u8],
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
noise_protocol: &mut NoiseProtocol,
|
||||
) -> Result<(), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let msg2 = noise_protocol
|
||||
.get_bytes_to_send()
|
||||
.ok_or_else(|| LpError::kkt_psq_handshake("failed to generate noise msg2"))??;
|
||||
// Embed PSK handle in message: [u16 handle_len][handle_bytes][noise_msg]
|
||||
let handle_len = psk_handle.len() as u16;
|
||||
let mut combined = Vec::with_capacity(2 + psk_handle.len() + msg2.len());
|
||||
combined.extend_from_slice(&handle_len.to_le_bytes());
|
||||
combined.extend_from_slice(psk_handle);
|
||||
combined.extend_from_slice(&msg2);
|
||||
|
||||
let lp_message = HandshakeData::new(combined).into();
|
||||
let lp_packet = self.next_packet(session_id, protocol, lp_message);
|
||||
self.connection
|
||||
.send_packet(lp_packet, Some(outer_aead_key))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to receive and process final PSQ msg3
|
||||
pub(crate) async fn receive_final_psq_message(
|
||||
&mut self,
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
noise_protocol: &mut NoiseProtocol,
|
||||
) -> Result<(), LpError> {
|
||||
let psq_msg3 = match self
|
||||
.connection
|
||||
.receive_packet(Some(outer_aead_key))
|
||||
.await?
|
||||
.message
|
||||
{
|
||||
LpMessage::Handshake(response) => response.0,
|
||||
other => {
|
||||
return Err(LpError::unexpected_handshake_response(
|
||||
other.typ(),
|
||||
MessageType::Handshake,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
noise_protocol.read_message(&psq_msg3)?;
|
||||
if !noise_protocol.is_handshake_finished() {
|
||||
return Err(LpError::kkt_psq_handshake(
|
||||
"noise handshake not finished after msg3",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send final ACK to indicate finalisation of the handshake
|
||||
pub(crate) async fn send_final_ack(
|
||||
&mut self,
|
||||
session_id: u32,
|
||||
outer_aead_key: &OuterAeadKey,
|
||||
) -> Result<(), LpError> {
|
||||
let protocol = self.protocol_version()?;
|
||||
|
||||
let ack = self.next_packet(session_id, protocol, LpMessage::Ack);
|
||||
self.connection
|
||||
.send_packet(ack, Some(outer_aead_key))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn complete_as_responder_inner(
|
||||
&mut self,
|
||||
) -> Result<LpSession, IntermediateHandshakeFailure>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
// 1. receive and validate ClientHello
|
||||
let (client_hello_data, remote_peer) =
|
||||
self.receive_client_hello()
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: None,
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
debug!("received client hello");
|
||||
|
||||
let session_id = client_hello_data.receiver_index;
|
||||
let session_id_bytes = session_id.to_le_bytes();
|
||||
let salt = client_hello_data.salt;
|
||||
|
||||
// 2. send ack
|
||||
debug!("sending client hello ACK");
|
||||
self.send_client_hello_ack(session_id)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
|
||||
// 3. receive and process KKT request
|
||||
let kkt_data =
|
||||
self.receive_kkt_request()
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
debug!("received KKT request");
|
||||
|
||||
// TEMP: 'derive' KEM keys
|
||||
let (dec_key, enc_key) =
|
||||
self.encapsulated_kem_keys()
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
|
||||
// 4. prepare and send KKT response
|
||||
debug!("sending KKT response");
|
||||
self.send_kkt_response(session_id, kkt_data, &enc_key)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
|
||||
// 5. receive and process PSQ msg1
|
||||
debug!("received PSQ msg1");
|
||||
let (outer_aead_key, mut noise_protocol, pq_shared_secret, psk_handle) = self
|
||||
.receive_psq_initiator_message(
|
||||
&remote_peer,
|
||||
(&dec_key, &enc_key),
|
||||
&salt,
|
||||
&session_id_bytes,
|
||||
)
|
||||
.await
|
||||
.map_err(|source| IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: None,
|
||||
source,
|
||||
})?;
|
||||
|
||||
// 6. prepare and send PSQ msg2
|
||||
debug!("sending PSQ msg2");
|
||||
if let Err(source) = self
|
||||
.send_psq_responder_message(
|
||||
session_id,
|
||||
&psk_handle,
|
||||
&outer_aead_key,
|
||||
&mut noise_protocol,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
// 7. receive and process PSQ msg3
|
||||
debug!("received PSQ msg3");
|
||||
if let Err(source) = self
|
||||
.receive_final_psq_message(&outer_aead_key, &mut noise_protocol)
|
||||
.await
|
||||
{
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
// 8. [optionally] send ACK to finalise
|
||||
debug!("sending final ACK");
|
||||
if let Err(source) = self.send_final_ack(session_id, &outer_aead_key).await {
|
||||
return Err(IntermediateHandshakeFailure {
|
||||
session_id: Some(session_id),
|
||||
protocol_version: self.protocol_version,
|
||||
outer_aead_key: Some(outer_aead_key),
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
Ok(LpSession::new(
|
||||
session_id,
|
||||
self.protocol_version()
|
||||
.expect("protocol version is known at this point"),
|
||||
outer_aead_key,
|
||||
self.local_peer.clone(),
|
||||
remote_peer,
|
||||
pq_shared_secret,
|
||||
noise_protocol,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn complete_as_responder(mut self) -> Result<LpSession, LpError>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
match self.complete_as_responder_inner().await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => Err(self.try_send_error_packet(err).await),
|
||||
}
|
||||
}
|
||||
}
|
||||
+165
-1790
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
mod tests {
|
||||
use crate::codec::{parse_lp_packet, serialize_lp_packet};
|
||||
use crate::{
|
||||
LpError,
|
||||
LpError, SessionsMock,
|
||||
message::LpMessage,
|
||||
packet::{LpHeader, LpPacket, TRAILER_LEN},
|
||||
session_manager::SessionManager,
|
||||
@@ -44,214 +44,21 @@ mod tests {
|
||||
#[test]
|
||||
fn test_full_session_flow() {
|
||||
// 1. Initialize session manager
|
||||
let session_manager_1 = SessionManager::new();
|
||||
let session_manager_2 = SessionManager::new();
|
||||
let mut session_manager_1 = SessionManager::new();
|
||||
let mut session_manager_2 = SessionManager::new();
|
||||
|
||||
// 2. Generate Ed25519 keypairs for PSQ authentication
|
||||
let (a, b) = mock_peers();
|
||||
let receiver_index = 12345;
|
||||
let sessions = SessionsMock::mock_post_handshake(receiver_index);
|
||||
|
||||
// Use fixed receiver_index for deterministic test
|
||||
let receiver_index: u32 = 100001;
|
||||
|
||||
// Test salt
|
||||
let salt = [42u8; 32];
|
||||
|
||||
// 4. Create sessions using the pre-built Noise states
|
||||
let peer_a_sm = session_manager_1
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
true,
|
||||
a.clone(),
|
||||
b.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.expect("Failed to create session A");
|
||||
|
||||
let peer_b_sm = session_manager_2
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
false,
|
||||
b.clone(),
|
||||
a.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.expect("Failed to create session B");
|
||||
// 2. Create sessions using the pre-built Noise states
|
||||
let peer_a_sm = session_manager_1.create_session_state_machine(sessions.initiator);
|
||||
let peer_b_sm = session_manager_2.create_session_state_machine(sessions.responder);
|
||||
|
||||
// Verify session count
|
||||
assert_eq!(session_manager_1.session_count(), 1);
|
||||
assert_eq!(session_manager_2.session_count(), 1);
|
||||
|
||||
// Initialize KKT state for both sessions (test bypass)
|
||||
session_manager_1
|
||||
.init_kkt_for_test(peer_a_sm, b.x25519.public_key())
|
||||
.expect("Failed to init KKT for peer A");
|
||||
session_manager_2
|
||||
.init_kkt_for_test(peer_b_sm, a.x25519.public_key())
|
||||
.expect("Failed to init KKT for peer B");
|
||||
|
||||
// 5. Simulate Noise Handshake (Sans-IO)
|
||||
println!("Starting handshake simulation...");
|
||||
let mut i_msg_payload;
|
||||
let mut r_msg_payload = None;
|
||||
let mut rounds = 0;
|
||||
const MAX_ROUNDS: usize = 10;
|
||||
|
||||
// Prime initiator's first message
|
||||
i_msg_payload = session_manager_1
|
||||
.prepare_handshake_message(peer_a_sm)
|
||||
.transpose()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
i_msg_payload.is_some(),
|
||||
"Initiator did not produce initial message"
|
||||
);
|
||||
|
||||
while rounds < MAX_ROUNDS {
|
||||
rounds += 1;
|
||||
let mut did_exchange = false;
|
||||
|
||||
// === Initiator -> Responder ===
|
||||
if let Some(payload) = i_msg_payload.take() {
|
||||
did_exchange = true;
|
||||
println!(
|
||||
" Round {}: Initiator -> Responder ({} bytes)",
|
||||
rounds,
|
||||
payload.len()
|
||||
);
|
||||
|
||||
// A prepares packet
|
||||
let counter = session_manager_1.next_counter(receiver_index).unwrap();
|
||||
let message_a_to_b = create_test_packet(1, receiver_index, counter, payload);
|
||||
let mut encoded_msg = BytesMut::new();
|
||||
serialize_lp_packet(&message_a_to_b, &mut encoded_msg, None)
|
||||
.expect("A serialize failed");
|
||||
|
||||
// B parses packet and checks replay
|
||||
let decoded_packet = parse_lp_packet(&encoded_msg, None).expect("B parse failed");
|
||||
assert_eq!(decoded_packet.header.counter, counter);
|
||||
|
||||
// Check replay before processing handshake
|
||||
session_manager_2
|
||||
.receiving_counter_quick_check(peer_b_sm, decoded_packet.header.counter)
|
||||
.expect("B replay check failed (A->B)");
|
||||
|
||||
match session_manager_2
|
||||
.process_handshake_message(peer_b_sm, &decoded_packet.message)
|
||||
{
|
||||
Ok(_) => {
|
||||
// Mark counter only after successful processing
|
||||
session_manager_2
|
||||
.receiving_counter_mark(peer_b_sm, decoded_packet.header.counter)
|
||||
.expect("B mark counter failed");
|
||||
}
|
||||
Err(e) => panic!("Responder processing failed: {:?}", e),
|
||||
}
|
||||
// Check if responder needs to send a reply
|
||||
r_msg_payload = session_manager_2
|
||||
.prepare_handshake_message(peer_b_sm)
|
||||
.transpose()
|
||||
.unwrap();
|
||||
println!("{:?}", r_msg_payload);
|
||||
}
|
||||
|
||||
// Check completion
|
||||
if session_manager_1.is_handshake_complete(peer_a_sm).unwrap()
|
||||
&& session_manager_2.is_handshake_complete(peer_b_sm).unwrap()
|
||||
{
|
||||
println!("Handshake completed after Initiator->Responder message.");
|
||||
break;
|
||||
}
|
||||
|
||||
// === Responder -> Initiator ===
|
||||
if let Some(payload) = r_msg_payload.take() {
|
||||
did_exchange = true;
|
||||
println!(
|
||||
" Round {}: Responder -> Initiator ({} bytes)",
|
||||
rounds,
|
||||
payload.len()
|
||||
);
|
||||
|
||||
// B prepares packet
|
||||
let counter = session_manager_2.next_counter(peer_b_sm).unwrap();
|
||||
let message_b_to_a = create_test_packet(1, receiver_index, counter, payload);
|
||||
let mut encoded_msg = BytesMut::new();
|
||||
serialize_lp_packet(&message_b_to_a, &mut encoded_msg, None)
|
||||
.expect("B serialize failed");
|
||||
|
||||
// A parses packet and checks replay
|
||||
let decoded_packet = parse_lp_packet(&encoded_msg, None).expect("A parse failed");
|
||||
assert_eq!(decoded_packet.header.counter, counter);
|
||||
|
||||
// Check replay before processing handshake
|
||||
session_manager_1
|
||||
.receiving_counter_quick_check(peer_a_sm, decoded_packet.header.counter)
|
||||
.expect("A replay check failed (B->A)");
|
||||
|
||||
match session_manager_1
|
||||
.process_handshake_message(peer_a_sm, &decoded_packet.message)
|
||||
{
|
||||
Ok(_) => {
|
||||
// Mark counter only after successful processing
|
||||
session_manager_1
|
||||
.receiving_counter_mark(peer_a_sm, decoded_packet.header.counter)
|
||||
.expect("A mark counter failed");
|
||||
}
|
||||
Err(e) => panic!("Initiator processing failed: {:?}", e),
|
||||
}
|
||||
|
||||
// Check if initiator needs to send a reply
|
||||
i_msg_payload = session_manager_1
|
||||
.prepare_handshake_message(peer_a_sm)
|
||||
.transpose()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// println!("Initiator state: {}", session_manager_1.get_state(peer_a_sm).unwrap());
|
||||
// println!("Responder state: {}", session_manager_2.get_state(peer_b_sm).unwrap());
|
||||
|
||||
println!(
|
||||
"Initiator state: {}",
|
||||
session_manager_1.is_handshake_complete(peer_a_sm).unwrap()
|
||||
);
|
||||
println!(
|
||||
"Responder state: {}",
|
||||
session_manager_2.is_handshake_complete(peer_b_sm).unwrap()
|
||||
);
|
||||
|
||||
// Check completion again
|
||||
if session_manager_1.is_handshake_complete(peer_a_sm).unwrap()
|
||||
&& session_manager_2.is_handshake_complete(peer_b_sm).unwrap()
|
||||
{
|
||||
println!("Handshake completed after Responder->Initiator message.");
|
||||
|
||||
// Safety break if no messages were exchanged in a round
|
||||
if !did_exchange {
|
||||
println!("No messages exchanged in round {}, breaking.", rounds);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert!(rounds < MAX_ROUNDS, "Handshake loop exceeded max rounds");
|
||||
}
|
||||
assert!(
|
||||
session_manager_1.is_handshake_complete(peer_a_sm).unwrap(),
|
||||
"Initiator handshake did not complete"
|
||||
);
|
||||
assert!(
|
||||
session_manager_2.is_handshake_complete(peer_b_sm).unwrap(),
|
||||
"Responder handshake did not complete"
|
||||
);
|
||||
println!(
|
||||
"Handshake simulation completed successfully in {} rounds.",
|
||||
rounds
|
||||
);
|
||||
|
||||
// --- Handshake Complete ---
|
||||
|
||||
// 7. Simulate Data Transfer (Post-Handshake)
|
||||
// 3. Simulate Data Transfer (Post-Handshake)
|
||||
println!("Starting data transfer simulation...");
|
||||
let plaintext_a_to_b = b"Hello from A!";
|
||||
|
||||
@@ -328,7 +135,7 @@ mod tests {
|
||||
|
||||
println!("Data transfer simulation completed.");
|
||||
|
||||
// 8. Replay Protection Test (Data Packet)
|
||||
// 4. Replay Protection Test (Data Packet)
|
||||
println!("Testing data packet replay protection...");
|
||||
// Try to replay the last message from B to A
|
||||
// Need to re-encode because decode consumes the buffer
|
||||
@@ -359,7 +166,7 @@ mod tests {
|
||||
);
|
||||
println!("Data packet replay protection test passed.");
|
||||
|
||||
// 9. Test out-of-order packet reception (send counter N+1 before counter N)
|
||||
// 5. Test out-of-order packet reception (send counter N+1 before counter N)
|
||||
println!("Testing out-of-order data packet reception...");
|
||||
let counter_a_next = session_manager_1.next_counter(peer_a_sm).unwrap(); // Should be counter_a + 1
|
||||
let counter_a_skip = session_manager_1.next_counter(peer_a_sm).unwrap(); // Should be counter_a + 2
|
||||
@@ -405,7 +212,7 @@ mod tests {
|
||||
String::from_utf8_lossy(&decrypted_payload)
|
||||
);
|
||||
|
||||
// 10. Now send the skipped counter N message (should still work)
|
||||
// 6. Now send the skipped counter N message (should still work)
|
||||
println!("Testing delayed data packet reception...");
|
||||
// Prepare data for counter_a_next (N)
|
||||
let plaintext_delayed = b"Delayed message";
|
||||
@@ -453,7 +260,7 @@ mod tests {
|
||||
|
||||
println!("Delayed data packet reception test passed.");
|
||||
|
||||
// 11. Try to replay message with counter N (should fail)
|
||||
// 7. Try to replay message with counter N (should fail)
|
||||
println!("Testing replay of delayed packet...");
|
||||
let parsed_delayed_replay =
|
||||
parse_lp_packet(&encoded_delayed_copy, None).expect("Parse delayed replay failed");
|
||||
@@ -465,7 +272,7 @@ mod tests {
|
||||
"Should be a replay protection error"
|
||||
);
|
||||
|
||||
// 12. Session removal
|
||||
// 8. Session removal
|
||||
assert!(session_manager_1.remove_state_machine(receiver_index));
|
||||
assert_eq!(session_manager_1.session_count(), 0);
|
||||
|
||||
@@ -482,94 +289,21 @@ mod tests {
|
||||
#[test]
|
||||
fn test_bidirectional_communication() {
|
||||
// 1. Initialize session manager
|
||||
let session_manager_1 = SessionManager::new();
|
||||
let session_manager_2 = SessionManager::new();
|
||||
let mut session_manager_1 = SessionManager::new();
|
||||
let mut session_manager_2 = SessionManager::new();
|
||||
|
||||
// 2. Generate Ed25519 keypairs for PSQ authentication
|
||||
let (a, b) = mock_peers();
|
||||
let receiver_index = 12345;
|
||||
let sessions = SessionsMock::mock_post_handshake(receiver_index);
|
||||
|
||||
// Use fixed receiver_index for test
|
||||
let receiver_index: u32 = 100002;
|
||||
// 2. Create sessions using the pre-built Noise states
|
||||
let peer_a_sm = session_manager_1.create_session_state_machine(sessions.initiator);
|
||||
let peer_b_sm = session_manager_2.create_session_state_machine(sessions.responder);
|
||||
|
||||
// Test salt
|
||||
let salt = [43u8; 32];
|
||||
// Counters after handshake
|
||||
let mut counter_a = 0; // Next counter for A to send
|
||||
let mut counter_b = 0; // Next counter for B to send
|
||||
|
||||
let peer_a_sm = session_manager_1
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
true,
|
||||
a.clone(),
|
||||
b.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.expect("Failed to create session A");
|
||||
|
||||
let peer_b_sm = session_manager_2
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
false,
|
||||
b.clone(),
|
||||
a.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.expect("Failed to create session B");
|
||||
|
||||
// Initialize KKT state for both sessions (test bypass)
|
||||
session_manager_1
|
||||
.init_kkt_for_test(peer_a_sm, b.x25519.public_key())
|
||||
.expect("Failed to init KKT for peer A");
|
||||
session_manager_2
|
||||
.init_kkt_for_test(peer_b_sm, a.x25519.public_key())
|
||||
.expect("Failed to init KKT for peer B");
|
||||
|
||||
// Drive handshake to completion (simplified)
|
||||
let mut i_msg = session_manager_1
|
||||
.prepare_handshake_message(peer_a_sm)
|
||||
.transpose()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
session_manager_2
|
||||
.process_handshake_message(peer_b_sm, &i_msg)
|
||||
.unwrap();
|
||||
session_manager_2
|
||||
.receiving_counter_mark(peer_b_sm, 0)
|
||||
.unwrap(); // Assume counter 0 for first msg
|
||||
let r_msg = session_manager_2
|
||||
.prepare_handshake_message(peer_b_sm)
|
||||
.transpose()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
session_manager_1
|
||||
.process_handshake_message(peer_a_sm, &r_msg)
|
||||
.unwrap();
|
||||
session_manager_1
|
||||
.receiving_counter_mark(peer_a_sm, 0)
|
||||
.unwrap(); // Assume counter 0 for first msg
|
||||
i_msg = session_manager_1
|
||||
.prepare_handshake_message(peer_a_sm)
|
||||
.transpose()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
session_manager_2
|
||||
.process_handshake_message(peer_b_sm, &i_msg)
|
||||
.unwrap();
|
||||
session_manager_2
|
||||
.receiving_counter_mark(peer_b_sm, 1)
|
||||
.unwrap(); // Assume counter 1 for second msg from A
|
||||
|
||||
assert!(session_manager_1.is_handshake_complete(peer_a_sm).unwrap());
|
||||
assert!(session_manager_2.is_handshake_complete(peer_b_sm).unwrap());
|
||||
println!("Bidirectional test: Handshake complete.");
|
||||
|
||||
// Counters after handshake (A sent 2, B sent 1)
|
||||
let mut counter_a = 2; // Next counter for A to send
|
||||
let mut counter_b = 1; // Next counter for B to send
|
||||
|
||||
// 4. Send multiple encrypted messages both ways
|
||||
// 3. Send multiple encrypted messages both ways
|
||||
const NUM_MESSAGES: u64 = 5;
|
||||
for i in 0..NUM_MESSAGES {
|
||||
println!("Bidirectional test: Round {}", i);
|
||||
@@ -634,36 +368,30 @@ mod tests {
|
||||
// Peer A sent handshake(0), handshake(1) + 5 data packets = 7 total. Next send counter = 7.
|
||||
// Peer A received handshake(0) + 5 data packets = 6 total. Next expected recv counter = 6.
|
||||
assert_eq!(
|
||||
counter_a,
|
||||
2 + NUM_MESSAGES,
|
||||
counter_a, NUM_MESSAGES,
|
||||
"Peer A final send counter mismatch"
|
||||
);
|
||||
assert_eq!(
|
||||
total_recv_a,
|
||||
1 + NUM_MESSAGES,
|
||||
total_recv_a, NUM_MESSAGES,
|
||||
"Peer A total received count mismatch"
|
||||
); // Received 1 handshake + 5 data
|
||||
); // Received 5 data
|
||||
assert_eq!(
|
||||
next_recv_a,
|
||||
1 + NUM_MESSAGES,
|
||||
next_recv_a, NUM_MESSAGES,
|
||||
"Peer A next expected receive counter mismatch"
|
||||
); // Expected counter for msg from B
|
||||
|
||||
// Peer B sent handshake(0) + 5 data packets = 6 total. Next send counter = 6.
|
||||
// Peer B received handshake(0), handshake(1) + 5 data packets = 7 total. Next expected recv counter = 7.
|
||||
assert_eq!(
|
||||
counter_b,
|
||||
1 + NUM_MESSAGES,
|
||||
counter_b, NUM_MESSAGES,
|
||||
"Peer B final send counter mismatch"
|
||||
);
|
||||
assert_eq!(
|
||||
total_recv_b,
|
||||
2 + NUM_MESSAGES,
|
||||
total_recv_b, NUM_MESSAGES,
|
||||
"Peer B total received count mismatch"
|
||||
); // Received 2 handshake + 5 data
|
||||
); // Received 5 data
|
||||
assert_eq!(
|
||||
next_recv_b,
|
||||
2 + NUM_MESSAGES,
|
||||
next_recv_b, NUM_MESSAGES,
|
||||
"Peer B next expected receive counter mismatch"
|
||||
); // Expected counter for msg from A
|
||||
|
||||
@@ -674,28 +402,14 @@ mod tests {
|
||||
#[test]
|
||||
fn test_session_error_handling() {
|
||||
// 1. Initialize session manager
|
||||
let session_manager = SessionManager::new();
|
||||
let mut session_manager = SessionManager::new();
|
||||
|
||||
// Generate Ed25519 keypair for PSQ authentication
|
||||
let (a, b) = mock_peers();
|
||||
|
||||
// Use fixed receiver_index for test
|
||||
let receiver_index: u32 = 100003;
|
||||
|
||||
// Test salt
|
||||
let salt = [44u8; 32];
|
||||
let receiver_index = 123;
|
||||
let session1 = SessionsMock::mock_post_handshake(receiver_index).initiator;
|
||||
let session2 = SessionsMock::mock_post_handshake(124).initiator;
|
||||
|
||||
// 2. Create a session (using real noise state)
|
||||
let _session = session_manager
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
true,
|
||||
a.clone(),
|
||||
b.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.expect("Failed to create session");
|
||||
let _session = session_manager.create_session_state_machine(session1);
|
||||
|
||||
// 3. Try to get a non-existent session
|
||||
let result = session_manager.state_machine_exists(999);
|
||||
@@ -709,20 +423,10 @@ mod tests {
|
||||
);
|
||||
|
||||
// 5. Create and immediately remove a session
|
||||
let receiver_index_temp: u32 = 100004;
|
||||
let _temp_session = session_manager
|
||||
.create_session_state_machine(
|
||||
receiver_index_temp,
|
||||
true,
|
||||
a.clone(),
|
||||
b.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.expect("Failed to create temp session");
|
||||
let _temp_session = session_manager.create_session_state_machine(session2);
|
||||
|
||||
assert!(
|
||||
session_manager.remove_state_machine(receiver_index_temp),
|
||||
session_manager.remove_state_machine(124),
|
||||
"Should remove the session"
|
||||
);
|
||||
|
||||
@@ -769,15 +473,8 @@ mod tests {
|
||||
}
|
||||
// Remove unused imports if SessionManager methods are no longer direct dependencies
|
||||
// use crate::noise_protocol::{create_noise_state, create_noise_state_responder};
|
||||
use crate::packet::version;
|
||||
use crate::peer::mock_peers;
|
||||
use crate::state_machine::LpData;
|
||||
use crate::{
|
||||
// Bring in state machine types
|
||||
state_machine::{LpAction, LpInput, LpStateBare},
|
||||
// message::LpMessage, // LpMessage likely still needed for LpInput/LpAction
|
||||
// packet::{LpHeader, LpPacket, TRAILER_LEN}, // LpPacket needed for LpAction/LpInput
|
||||
};
|
||||
use crate::state_machine::{LpAction, LpInput, LpStateBare};
|
||||
// Use Bytes for SendData input
|
||||
|
||||
// Keep helper function for creating test packets if needed,
|
||||
@@ -794,309 +491,22 @@ mod tests {
|
||||
#[test]
|
||||
fn test_full_session_flow_with_process_input() {
|
||||
// 1. Initialize session managers
|
||||
let session_manager_1 = SessionManager::new();
|
||||
let session_manager_2 = SessionManager::new();
|
||||
let mut session_manager_1 = SessionManager::new();
|
||||
let mut session_manager_2 = SessionManager::new();
|
||||
|
||||
// 2. Generate Ed25519 keypairs for PSQ authentication
|
||||
let (a, b) = mock_peers();
|
||||
let receiver_index = 12345;
|
||||
let sessions = SessionsMock::mock_post_handshake(receiver_index);
|
||||
|
||||
// Use fixed receiver_index for test
|
||||
let receiver_index: u32 = 100005;
|
||||
|
||||
// Test salt
|
||||
let salt = [45u8; 32];
|
||||
|
||||
// 3. Create sessions state machines
|
||||
assert!(
|
||||
session_manager_1
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
true,
|
||||
a.clone(),
|
||||
b.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT
|
||||
) // Initiator
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
session_manager_2
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
false,
|
||||
b,
|
||||
a.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT
|
||||
) // Responder
|
||||
.is_ok()
|
||||
);
|
||||
// 2. Create sessions state machines
|
||||
session_manager_1.create_session_state_machine(sessions.initiator);
|
||||
session_manager_2.create_session_state_machine(sessions.responder);
|
||||
|
||||
assert_eq!(session_manager_1.session_count(), 1);
|
||||
assert_eq!(session_manager_2.session_count(), 1);
|
||||
assert!(session_manager_1.state_machine_exists(receiver_index));
|
||||
assert!(session_manager_2.state_machine_exists(receiver_index));
|
||||
|
||||
// Verify initial states are ReadyToHandshake
|
||||
assert_eq!(
|
||||
session_manager_1.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::ReadyToHandshake
|
||||
);
|
||||
assert_eq!(
|
||||
session_manager_2.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::ReadyToHandshake
|
||||
);
|
||||
|
||||
// --- 4. Simulate Noise Handshake via process_input ---
|
||||
println!("Starting handshake simulation via process_input...");
|
||||
|
||||
let mut packet_a_to_b: Option<LpPacket>;
|
||||
let mut packet_b_to_a: Option<LpPacket>;
|
||||
let mut rounds = 0;
|
||||
const MAX_ROUNDS: usize = 10; // KKT (2 messages) + XK handshake (3 messages) + PSQ = 6 rounds total
|
||||
|
||||
// --- Round 1: Initiator Starts ---
|
||||
println!(" Round {}: Initiator starts handshake", rounds);
|
||||
let action_a1 = session_manager_1
|
||||
.process_input(receiver_index, LpInput::StartHandshake)
|
||||
.expect("Initiator StartHandshake should produce an action")
|
||||
.expect("Initiator StartHandshake failed");
|
||||
|
||||
if let LpAction::SendPacket(packet) = action_a1 {
|
||||
println!(" Initiator produced SendPacket (KKT request)");
|
||||
packet_a_to_b = Some(packet);
|
||||
} else {
|
||||
panic!("Initiator StartHandshake did not produce SendPacket");
|
||||
}
|
||||
// After StartHandshake, initiator should be in KKTExchange state (not Handshaking yet)
|
||||
assert_eq!(
|
||||
session_manager_1.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::KKTExchange,
|
||||
"Initiator state wrong after StartHandshake (should be KKTExchange)"
|
||||
);
|
||||
|
||||
// *** ADD THIS BLOCK for Responder StartHandshake ***
|
||||
println!(
|
||||
" Round {}: Responder explicitly enters KKTExchange state",
|
||||
rounds
|
||||
);
|
||||
let action_b_start =
|
||||
session_manager_2.process_input(receiver_index, LpInput::StartHandshake);
|
||||
// Responder's StartHandshake should not produce an action to send
|
||||
assert!(
|
||||
action_b_start.as_ref().unwrap().is_none(),
|
||||
"Responder StartHandshake should produce None action, got {:?}",
|
||||
action_b_start
|
||||
);
|
||||
// Verify responder transitions to KKTExchange state (not Handshaking yet)
|
||||
assert_eq!(
|
||||
session_manager_2.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::KKTExchange, // Responder also enters KKTExchange state
|
||||
"Responder state should be KKTExchange after its StartHandshake"
|
||||
);
|
||||
// *** END OF ADDED BLOCK ***
|
||||
|
||||
// --- Round 2: Responder Receives KKT Request, Sends KKT Response ---
|
||||
rounds += 1;
|
||||
println!(
|
||||
" Round {}: Responder receives KKT request, sends KKT response",
|
||||
rounds
|
||||
);
|
||||
let packet_to_process = packet_a_to_b
|
||||
.take()
|
||||
.expect("KKT request from A was missing");
|
||||
|
||||
// Simulate network: serialize -> parse (optional but good practice)
|
||||
let mut buf_a = BytesMut::new();
|
||||
serialize_lp_packet(&packet_to_process, &mut buf_a, None).unwrap();
|
||||
let parsed_packet_a = parse_lp_packet(&buf_a, None).unwrap();
|
||||
|
||||
// Responder processes KKT request
|
||||
let action_b1 = session_manager_2
|
||||
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_a))
|
||||
.expect("Responder ReceivePacket should produce an action")
|
||||
.expect("Responder ReceivePacket failed");
|
||||
|
||||
if let LpAction::SendPacket(packet) = action_b1 {
|
||||
println!(" Responder received KKT request, produced KKT response");
|
||||
packet_b_to_a = Some(packet);
|
||||
} else {
|
||||
panic!("Responder ReceivePacket did not produce SendPacket for KKT response");
|
||||
}
|
||||
// Responder transitions to Handshaking after KKT completes
|
||||
assert_eq!(
|
||||
session_manager_2.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::Handshaking,
|
||||
"Responder state should be Handshaking after KKT exchange"
|
||||
);
|
||||
|
||||
// --- Round 3: Initiator Receives KKT Response, Sends First Noise Message (with PSQ) ---
|
||||
rounds += 1;
|
||||
println!(
|
||||
" Round {}: Initiator receives KKT response, sends first Noise message (with PSQ)",
|
||||
rounds
|
||||
);
|
||||
let packet_to_process = packet_b_to_a
|
||||
.take()
|
||||
.expect("KKT response from B was missing");
|
||||
|
||||
// Simulate network
|
||||
let mut buf_b = BytesMut::new();
|
||||
serialize_lp_packet(&packet_to_process, &mut buf_b, None).unwrap();
|
||||
let parsed_packet_b = parse_lp_packet(&buf_b, None).unwrap();
|
||||
|
||||
// Initiator processes KKT response
|
||||
let action_a2 = session_manager_1
|
||||
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_b))
|
||||
.expect("Initiator ReceivePacket should produce an action")
|
||||
.expect("Initiator ReceivePacket failed");
|
||||
|
||||
match action_a2 {
|
||||
LpAction::SendPacket(packet) => {
|
||||
println!(
|
||||
" Initiator received KKT response, produced first Noise message (-> e)"
|
||||
);
|
||||
packet_a_to_b = Some(packet);
|
||||
// Initiator transitions to Handshaking after KKT completes
|
||||
assert_eq!(
|
||||
session_manager_1.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::Handshaking,
|
||||
"Initiator state should be Handshaking after receiving KKT response"
|
||||
);
|
||||
}
|
||||
LpAction::KKTComplete => {
|
||||
println!(
|
||||
" Initiator received KKT response, produced KKTComplete (will send Noise in next step)"
|
||||
);
|
||||
// KKT completed, now need to explicitly trigger handshake message
|
||||
// This might be the case if KKT completion doesn't automatically send the first Noise message
|
||||
// Let's try to prepare the handshake message
|
||||
if let Some(msg_result) =
|
||||
session_manager_1.prepare_handshake_message(receiver_index)
|
||||
{
|
||||
let msg = msg_result.expect("Failed to prepare handshake message after KKT");
|
||||
// Create a packet from the message
|
||||
let packet = create_test_packet(1, receiver_index, 0, msg);
|
||||
packet_a_to_b = Some(packet);
|
||||
println!(" Prepared first Noise message after KKTComplete");
|
||||
} else {
|
||||
panic!("No handshake message available after KKT complete");
|
||||
}
|
||||
}
|
||||
other => {
|
||||
panic!(
|
||||
"Initiator ReceivePacket produced unexpected action after KKT response: {:?}",
|
||||
other
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Round 4: Responder Receives First Noise Message, Sends Second ---
|
||||
rounds += 1;
|
||||
println!(
|
||||
" Round {}: Responder receives first Noise message, sends second",
|
||||
rounds
|
||||
);
|
||||
let packet_to_process = packet_a_to_b
|
||||
.take()
|
||||
.expect("First Noise packet from A was missing");
|
||||
|
||||
// Simulate network
|
||||
let mut buf_a2 = BytesMut::new();
|
||||
serialize_lp_packet(&packet_to_process, &mut buf_a2, None).unwrap();
|
||||
let parsed_packet_a2 = parse_lp_packet(&buf_a2, None).unwrap();
|
||||
|
||||
// Responder processes first Noise message and sends second Noise message
|
||||
let action_b2 = session_manager_2
|
||||
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_a2))
|
||||
.expect("Responder ReceivePacket should produce an action")
|
||||
.expect("Responder ReceivePacket failed");
|
||||
|
||||
if let LpAction::SendPacket(packet) = action_b2 {
|
||||
println!(
|
||||
" Responder received first Noise message, produced second Noise message (<- e, ee, s, es)"
|
||||
);
|
||||
packet_b_to_a = Some(packet);
|
||||
} else {
|
||||
panic!("Responder did not produce SendPacket for second Noise message");
|
||||
}
|
||||
// Responder still in Handshaking, waiting for final message
|
||||
assert_eq!(
|
||||
session_manager_2.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::Handshaking,
|
||||
"Responder state should still be Handshaking after sending second message"
|
||||
);
|
||||
|
||||
// --- Round 5: Initiator Receives Second Noise Message, Sends Third, Completes ---
|
||||
rounds += 1;
|
||||
println!(
|
||||
" Round {}: Initiator receives second Noise message, sends third, completes",
|
||||
rounds
|
||||
);
|
||||
let packet_to_process = packet_b_to_a
|
||||
.take()
|
||||
.expect("Second Noise packet from B was missing");
|
||||
|
||||
let mut buf_b2 = BytesMut::new();
|
||||
serialize_lp_packet(&packet_to_process, &mut buf_b2, None).unwrap();
|
||||
let parsed_packet_b2 = parse_lp_packet(&buf_b2, None).unwrap();
|
||||
|
||||
let action_a3 = session_manager_1
|
||||
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_b2))
|
||||
.expect("Initiator ReceivePacket should produce an action")
|
||||
.expect("Initiator ReceivePacket failed");
|
||||
|
||||
if let LpAction::SendPacket(packet) = action_a3 {
|
||||
println!(
|
||||
" Initiator received second Noise message, produced third Noise message (-> s, se)"
|
||||
);
|
||||
packet_a_to_b = Some(packet);
|
||||
} else {
|
||||
panic!("Initiator did not produce SendPacket for third Noise message");
|
||||
}
|
||||
// Initiator transitions to Transport after sending third message
|
||||
assert_eq!(
|
||||
session_manager_1.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::Transport,
|
||||
"Initiator state should be Transport after sending third message"
|
||||
);
|
||||
|
||||
// --- Round 6: Responder Receives Third Noise Message, Completes ---
|
||||
rounds += 1;
|
||||
println!(
|
||||
" Round {}: Responder receives third Noise message, completes",
|
||||
rounds
|
||||
);
|
||||
let packet_to_process = packet_a_to_b
|
||||
.take()
|
||||
.expect("Third Noise packet from A was missing");
|
||||
|
||||
let mut buf_a3 = BytesMut::new();
|
||||
serialize_lp_packet(&packet_to_process, &mut buf_a3, None).unwrap();
|
||||
let parsed_packet_a3 = parse_lp_packet(&buf_a3, None).unwrap();
|
||||
|
||||
let action_b3 = session_manager_2
|
||||
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_a3))
|
||||
.expect("Responder final ReceivePacket should produce an action")
|
||||
.expect("Responder final ReceivePacket failed");
|
||||
|
||||
// Responder completes handshake
|
||||
if let LpAction::HandshakeComplete = action_b3 {
|
||||
println!(" Responder received third Noise message, produced HandshakeComplete");
|
||||
} else {
|
||||
println!(
|
||||
" Responder received third Noise message (Action: {:?})",
|
||||
action_b3
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
session_manager_2.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::Transport,
|
||||
"Responder state should be Transport after processing third message"
|
||||
);
|
||||
|
||||
// --- Verification ---
|
||||
assert!(rounds < MAX_ROUNDS, "Handshake took too many rounds");
|
||||
// Verify initial states are Transport
|
||||
assert_eq!(
|
||||
session_manager_1.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::Transport
|
||||
@@ -1105,9 +515,8 @@ mod tests {
|
||||
session_manager_2.get_state(receiver_index).unwrap(),
|
||||
LpStateBare::Transport
|
||||
);
|
||||
println!("Handshake simulation completed successfully via process_input.");
|
||||
|
||||
// --- 5. Simulate Data Transfer via process_input ---
|
||||
// --- 3. Simulate Data Transfer via process_input ---
|
||||
println!("Starting data transfer simulation via process_input...");
|
||||
let plaintext_a_to_b = LpData::new_opaque(b"Hello from A via process_input!".to_vec());
|
||||
let plaintext_b_to_a = LpData::new_opaque(b"Hello from B via process_input!".to_vec());
|
||||
@@ -1185,7 +594,7 @@ mod tests {
|
||||
}
|
||||
println!("Data transfer simulation completed.");
|
||||
|
||||
// --- 6. Replay Protection Test ---
|
||||
// --- 4. Replay Protection Test ---
|
||||
println!("Testing data packet replay protection via process_input...");
|
||||
let replay_result = session_manager_1
|
||||
.process_input(receiver_index, LpInput::ReceivePacket(data_packet_b_replay)); // Use cloned packet
|
||||
@@ -1199,7 +608,7 @@ mod tests {
|
||||
);
|
||||
println!("Data packet replay protection test passed.");
|
||||
|
||||
// --- 7. Out-of-Order Test ---
|
||||
// --- 5. Out-of-Order Test ---
|
||||
println!("Testing out-of-order reception via process_input...");
|
||||
|
||||
// A prepares N+1 then N
|
||||
@@ -1258,7 +667,7 @@ mod tests {
|
||||
);
|
||||
println!("Out-of-order test passed.");
|
||||
|
||||
// --- 8. Close Test ---
|
||||
// --- 6. Close Test ---
|
||||
println!("Testing close via process_input...");
|
||||
|
||||
// A closes
|
||||
@@ -1306,7 +715,7 @@ mod tests {
|
||||
));
|
||||
println!("Close test passed.");
|
||||
|
||||
// --- 9. Session Removal ---
|
||||
// --- 7. Session Removal ---
|
||||
assert!(session_manager_1.remove_state_machine(receiver_index));
|
||||
assert_eq!(session_manager_1.session_count(), 0);
|
||||
assert!(!session_manager_1.state_machine_exists(receiver_index));
|
||||
|
||||
@@ -6,11 +6,9 @@
|
||||
//! This module implements session lifecycle management functionality, handling
|
||||
//! creation, retrieval, and storage of sessions.
|
||||
|
||||
use crate::noise_protocol::ReadResult;
|
||||
use crate::peer::{LpLocalPeer, LpRemotePeer};
|
||||
use crate::state_machine::{LpAction, LpInput, LpState, LpStateBare};
|
||||
use crate::state_machine::{LpAction, LpInput, LpStateBare};
|
||||
use crate::{LpError, LpMessage, LpSession, LpStateMachine};
|
||||
use dashmap::DashMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Manages the lifecycle of Lewes Protocol sessions.
|
||||
///
|
||||
@@ -18,7 +16,7 @@ use dashmap::DashMap;
|
||||
/// ensuring proper thread-safety for concurrent access.
|
||||
pub struct SessionManager {
|
||||
/// Manages state machines directly, keyed by lp_id
|
||||
state_machines: DashMap<u32, LpStateMachine>,
|
||||
state_machines: HashMap<u32, LpStateMachine>,
|
||||
}
|
||||
|
||||
impl Default for SessionManager {
|
||||
@@ -31,36 +29,18 @@ impl SessionManager {
|
||||
/// Creates a new session manager with empty session storage.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state_machines: DashMap::new(),
|
||||
state_machines: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_input(&self, lp_id: u32, input: LpInput) -> Result<Option<LpAction>, LpError> {
|
||||
pub fn process_input(
|
||||
&mut self,
|
||||
lp_id: u32,
|
||||
input: LpInput,
|
||||
) -> Result<Option<LpAction>, LpError> {
|
||||
self.with_state_machine_mut(lp_id, |sm| sm.process_input(input).transpose())?
|
||||
}
|
||||
|
||||
pub fn add(&self, session: LpSession) -> Result<(), LpError> {
|
||||
let sm = LpStateMachine {
|
||||
state: LpState::ReadyToHandshake {
|
||||
session: Box::new(session),
|
||||
},
|
||||
};
|
||||
self.state_machines.insert(sm.id()?, sm);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handshaking(&self, lp_id: u32) -> Result<bool, LpError> {
|
||||
Ok(self.get_state(lp_id)? == LpStateBare::Handshaking)
|
||||
}
|
||||
|
||||
pub fn should_initiate_handshake(&self, lp_id: u32) -> Result<bool, LpError> {
|
||||
Ok(self.ready_to_handshake(lp_id)? || self.closed(lp_id)?)
|
||||
}
|
||||
|
||||
pub fn ready_to_handshake(&self, lp_id: u32) -> Result<bool, LpError> {
|
||||
Ok(self.get_state(lp_id)? == LpStateBare::ReadyToHandshake)
|
||||
}
|
||||
|
||||
pub fn closed(&self, lp_id: u32) -> Result<bool, LpError> {
|
||||
Ok(self.get_state(lp_id)? == LpStateBare::Closed)
|
||||
}
|
||||
@@ -84,38 +64,27 @@ impl SessionManager {
|
||||
})?
|
||||
}
|
||||
|
||||
pub fn receiving_counter_mark(&self, lp_id: u32, counter: u64) -> Result<(), LpError> {
|
||||
self.with_state_machine(lp_id, |sm| sm.session()?.receiving_counter_mark(counter))?
|
||||
pub fn receiving_counter_mark(&mut self, lp_id: u32, counter: u64) -> Result<(), LpError> {
|
||||
self.with_state_machine_mut(lp_id, |sm| {
|
||||
sm.session_mut()?.receiving_counter_mark(counter)
|
||||
})?
|
||||
}
|
||||
|
||||
pub fn start_handshake(&self, lp_id: u32) -> Option<Result<LpMessage, LpError>> {
|
||||
self.prepare_handshake_message(lp_id)
|
||||
pub fn next_counter(&mut self, lp_id: u32) -> Result<u64, LpError> {
|
||||
self.with_state_machine_mut(lp_id, |sm| Ok(sm.session_mut()?.next_counter()))?
|
||||
}
|
||||
|
||||
pub fn prepare_handshake_message(&self, lp_id: u32) -> Option<Result<LpMessage, LpError>> {
|
||||
self.with_state_machine(lp_id, |sm| sm.session().ok()?.prepare_handshake_message())
|
||||
.ok()?
|
||||
}
|
||||
|
||||
pub fn is_handshake_complete(&self, lp_id: u32) -> Result<bool, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| Ok(sm.session()?.is_handshake_complete()))?
|
||||
}
|
||||
|
||||
pub fn next_counter(&self, lp_id: u32) -> Result<u64, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| Ok(sm.session()?.next_counter()))?
|
||||
}
|
||||
|
||||
pub fn decrypt_data(&self, lp_id: u32, message: &LpMessage) -> Result<Vec<u8>, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| {
|
||||
sm.session()?
|
||||
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(&self, lp_id: u32, message: &[u8]) -> Result<LpMessage, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| {
|
||||
sm.session()?
|
||||
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)
|
||||
})?
|
||||
@@ -125,14 +94,6 @@ impl SessionManager {
|
||||
self.with_state_machine(lp_id, |sm| Ok(sm.session()?.current_packet_cnt()))?
|
||||
}
|
||||
|
||||
pub fn process_handshake_message(
|
||||
&self,
|
||||
lp_id: u32,
|
||||
message: &LpMessage,
|
||||
) -> Result<ReadResult, LpError> {
|
||||
self.with_state_machine(lp_id, |sm| sm.session()?.process_handshake_message(message))?
|
||||
}
|
||||
|
||||
pub fn session_count(&self) -> usize {
|
||||
self.state_machines.len()
|
||||
}
|
||||
@@ -146,7 +107,7 @@ impl SessionManager {
|
||||
F: FnOnce(&LpStateMachine) -> R,
|
||||
{
|
||||
if let Some(sm) = self.state_machines.get(&lp_id) {
|
||||
Ok(f(&sm))
|
||||
Ok(f(sm))
|
||||
} else {
|
||||
Err(LpError::StateMachineNotFound { lp_id })
|
||||
}
|
||||
@@ -154,90 +115,48 @@ impl SessionManager {
|
||||
}
|
||||
|
||||
// For mutable access (like running process_input)
|
||||
pub fn with_state_machine_mut<F, R>(&self, lp_id: u32, f: F) -> Result<R, LpError>
|
||||
pub fn with_state_machine_mut<F, R>(&mut self, lp_id: u32, f: F) -> Result<R, LpError>
|
||||
where
|
||||
F: FnOnce(&mut LpStateMachine) -> R, // Closure takes mutable ref
|
||||
{
|
||||
if let Some(mut sm) = self.state_machines.get_mut(&lp_id) {
|
||||
Ok(f(&mut sm))
|
||||
if let Some(sm) = self.state_machines.get_mut(&lp_id) {
|
||||
Ok(f(sm))
|
||||
} else {
|
||||
Err(LpError::StateMachineNotFound { lp_id })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_session_state_machine(
|
||||
&self,
|
||||
receiver_index: u32,
|
||||
is_initiator: bool,
|
||||
local_peer: LpLocalPeer,
|
||||
remote_peer: LpRemotePeer,
|
||||
salt: &[u8; 32],
|
||||
protocol_version: u8,
|
||||
) -> Result<u32, LpError> {
|
||||
let sm = LpStateMachine::new(
|
||||
receiver_index,
|
||||
is_initiator,
|
||||
local_peer,
|
||||
remote_peer,
|
||||
salt,
|
||||
protocol_version,
|
||||
)?;
|
||||
|
||||
pub fn create_session_state_machine(&mut self, lp_session: LpSession) -> u32 {
|
||||
let receiver_index = lp_session.id();
|
||||
let sm = LpStateMachine::new(lp_session);
|
||||
self.state_machines.insert(receiver_index, sm);
|
||||
Ok(receiver_index)
|
||||
receiver_index
|
||||
}
|
||||
|
||||
/// Method to remove a state machine
|
||||
pub fn remove_state_machine(&self, lp_id: u32) -> bool {
|
||||
pub fn remove_state_machine(&mut self, lp_id: u32) -> bool {
|
||||
let removed = self.state_machines.remove(&lp_id);
|
||||
|
||||
removed.is_some()
|
||||
}
|
||||
|
||||
/// Test-only method to initialize KKT state to Completed for a session.
|
||||
/// This allows integration tests to bypass KKT exchange and directly test PSQ/handshake.
|
||||
#[cfg(test)]
|
||||
pub fn init_kkt_for_test(
|
||||
&self,
|
||||
lp_id: u32,
|
||||
remote_x25519_pub: &nym_crypto::asymmetric::x25519::PublicKey,
|
||||
) -> Result<(), LpError> {
|
||||
self.with_state_machine(lp_id, |sm| {
|
||||
sm.session()?.set_kkt_completed_for_test(remote_x25519_pub);
|
||||
Ok(())
|
||||
})?
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::packet::version;
|
||||
use crate::peer::{mock_peers, random_peer};
|
||||
use nym_test_utils::helpers::deterministic_rng;
|
||||
use crate::{SessionsMock, mock_session_for_test};
|
||||
|
||||
#[test]
|
||||
fn test_session_manager_get() {
|
||||
let manager = SessionManager::new();
|
||||
let mut rng = deterministic_rng();
|
||||
let local = random_peer(&mut rng);
|
||||
let peer1 = random_peer(&mut rng);
|
||||
let mut manager = SessionManager::new();
|
||||
|
||||
let salt = [47u8; 32];
|
||||
let receiver_index: u32 = 1001;
|
||||
let local_session = mock_session_for_test();
|
||||
let id = local_session.id();
|
||||
|
||||
let sm_1_id = manager
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
true,
|
||||
local,
|
||||
peer1.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.unwrap();
|
||||
let sm_1_id = manager.create_session_state_machine(local_session);
|
||||
assert_eq!(sm_1_id, id);
|
||||
|
||||
let retrieved = manager.state_machine_exists(sm_1_id);
|
||||
let retrieved = manager.state_machine_exists(id);
|
||||
assert!(retrieved);
|
||||
|
||||
let not_found = manager.state_machine_exists(99);
|
||||
@@ -246,24 +165,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_session_manager_remove() {
|
||||
let manager = SessionManager::new();
|
||||
let mut rng = deterministic_rng();
|
||||
let local = random_peer(&mut rng);
|
||||
let peer1 = random_peer(&mut rng);
|
||||
let mut manager = SessionManager::new();
|
||||
let local_session = mock_session_for_test();
|
||||
|
||||
let salt = [48u8; 32];
|
||||
let receiver_index: u32 = 2002;
|
||||
|
||||
let sm_1_id = manager
|
||||
.create_session_state_machine(
|
||||
receiver_index,
|
||||
true,
|
||||
local,
|
||||
peer1.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.unwrap();
|
||||
let sm_1_id = manager.create_session_state_machine(local_session);
|
||||
|
||||
let removed = manager.remove_state_machine(sm_1_id);
|
||||
assert!(removed);
|
||||
@@ -275,47 +180,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_multiple_sessions() {
|
||||
let manager = SessionManager::new();
|
||||
let mut rng = deterministic_rng();
|
||||
let local = random_peer(&mut rng);
|
||||
let peer1 = random_peer(&mut rng);
|
||||
let peer2 = random_peer(&mut rng);
|
||||
let peer3 = random_peer(&mut rng);
|
||||
let mut manager = SessionManager::new();
|
||||
let session1 = SessionsMock::mock_post_handshake(123).initiator;
|
||||
let session2 = SessionsMock::mock_post_handshake(124).initiator;
|
||||
let session3 = SessionsMock::mock_post_handshake(125).initiator;
|
||||
|
||||
let salt = [49u8; 32];
|
||||
|
||||
let sm_1 = manager
|
||||
.create_session_state_machine(
|
||||
3001,
|
||||
true,
|
||||
local.clone(),
|
||||
peer1.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sm_2 = manager
|
||||
.create_session_state_machine(
|
||||
3002,
|
||||
true,
|
||||
local.clone(),
|
||||
peer2.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sm_3 = manager
|
||||
.create_session_state_machine(
|
||||
3003,
|
||||
true,
|
||||
local.clone(),
|
||||
peer3.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
)
|
||||
.unwrap();
|
||||
let sm_1 = manager.create_session_state_machine(session1);
|
||||
let sm_2 = manager.create_session_state_machine(session2);
|
||||
let sm_3 = manager.create_session_state_machine(session3);
|
||||
|
||||
assert_eq!(manager.session_count(), 3);
|
||||
|
||||
@@ -330,24 +202,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_session_manager_create_session() {
|
||||
let manager = SessionManager::new();
|
||||
let (init, resp) = mock_peers();
|
||||
let mut manager = SessionManager::new();
|
||||
|
||||
let salt = [50u8; 32];
|
||||
let receiver_index: u32 = 4004;
|
||||
|
||||
let sm = manager.create_session_state_machine(
|
||||
receiver_index,
|
||||
true,
|
||||
init,
|
||||
resp.as_remote(),
|
||||
&salt,
|
||||
version::CURRENT,
|
||||
);
|
||||
|
||||
assert!(sm.is_ok());
|
||||
let sm = sm.unwrap();
|
||||
let sesion = mock_session_for_test();
|
||||
|
||||
let sm = manager.create_session_state_machine(sesion);
|
||||
assert_eq!(manager.session_count(), 1);
|
||||
|
||||
let retrieved = manager.get_state_machine_id(sm);
|
||||
|
||||
+195
-847
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ rand_chacha = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync", "time", "rt"] }
|
||||
tracing = { workspace = true }
|
||||
|
||||
nym-bin-common = { workspace = true, features = ["tracing"] }
|
||||
nym-bin-common = { workspace = true, features = ["basic_tracing"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
|
||||
@@ -95,6 +95,7 @@ nym-gateway-storage = { workspace = true, features = ["mock"] }
|
||||
nym-wireguard = { workspace = true, features = ["mock"] }
|
||||
mock_instant = { workspace = true }
|
||||
time = { workspace = true }
|
||||
nym-lp = { path = "../common/nym-lp", features = ["mock"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -155,14 +155,11 @@ impl LpDataHandler {
|
||||
.value()
|
||||
.state
|
||||
.session()
|
||||
.map_err(|e| GatewayError::LpProtocolError(format!("Session error: {}", e)))?
|
||||
.outer_aead_key()
|
||||
.ok_or_else(|| {
|
||||
GatewayError::LpProtocolError("Session has no outer AEAD key".to_string())
|
||||
})?;
|
||||
.map_err(|e| GatewayError::LpProtocolError(format!("Session error: {e}")))?
|
||||
.outer_aead_key();
|
||||
|
||||
// Parse full packet with outer AEAD decryption
|
||||
let lp_packet = nym_lp::codec::parse_lp_packet(packet, Some(&outer_key)).map_err(|e| {
|
||||
let lp_packet = nym_lp::codec::parse_lp_packet(packet, Some(outer_key)).map_err(|e| {
|
||||
inc!("lp_data_decrypt_errors");
|
||||
GatewayError::LpProtocolError(format!("Failed to decrypt LP packet: {}", e))
|
||||
})?;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -362,15 +362,6 @@ pub struct LpHandlerState {
|
||||
/// from LP clients into the mixnet for routing.
|
||||
pub outbound_mix_sender: MixForwardingSender,
|
||||
|
||||
/// In-progress handshakes keyed by session_id
|
||||
///
|
||||
/// Session ID is deterministically computed from both parties' X25519 keys immediately
|
||||
/// after ClientHello. Used during handshake phase. After handshake completes,
|
||||
/// state moves to session_states map.
|
||||
///
|
||||
/// Wrapped in TimestampedState for TTL-based cleanup of stale handshakes.
|
||||
pub handshake_states: Arc<DashMap<ReceiverIndex, TimestampedState<LpStateMachine>>>,
|
||||
|
||||
/// Established sessions keyed by session_id
|
||||
///
|
||||
/// Used after handshake completes (session_id is deterministically computed from
|
||||
@@ -504,7 +495,7 @@ impl LpListener {
|
||||
handler::LpConnectionHandler::new(stream, remote_addr, self.handler_state.clone());
|
||||
|
||||
let metrics = self.handler_state.metrics.clone();
|
||||
self.shutdown.try_spawn_named(
|
||||
self.shutdown.try_spawn_named_with_shutdown(
|
||||
async move {
|
||||
let result = handler.handle().await;
|
||||
|
||||
@@ -559,7 +550,6 @@ impl LpListener {
|
||||
///
|
||||
/// The task automatically stops when the shutdown signal is received.
|
||||
fn spawn_state_cleanup_task(&self) -> tokio::task::JoinHandle<()> {
|
||||
let handshake_states = Arc::clone(&self.handler_state.handshake_states);
|
||||
let session_states = Arc::clone(&self.handler_state.session_states);
|
||||
let dbg_cfg = self.handler_state.lp_config.debug;
|
||||
|
||||
@@ -576,13 +566,7 @@ impl LpListener {
|
||||
);
|
||||
|
||||
self.shutdown.try_spawn_named(
|
||||
cleanup_task::cleanup_loop(
|
||||
handshake_states,
|
||||
session_states,
|
||||
dbg_cfg,
|
||||
shutdown,
|
||||
metrics,
|
||||
),
|
||||
cleanup_task::cleanup_loop(session_states, dbg_cfg, shutdown, metrics),
|
||||
"LP::StateCleanup",
|
||||
)
|
||||
}
|
||||
@@ -606,29 +590,16 @@ pub(crate) mod cleanup_task {
|
||||
use tracing::{debug, info};
|
||||
|
||||
async fn perform_cleanup(
|
||||
handshake_states: &Arc<DashMap<u32, TimestampedState<LpStateMachine>>>,
|
||||
session_states: &Arc<DashMap<u32, TimestampedState<LpStateMachine>>>,
|
||||
cfg: LpDebug,
|
||||
) {
|
||||
let handshake_ttl = cfg.handshake_ttl;
|
||||
let session_ttl = cfg.session_ttl;
|
||||
let demoted_session_ttl = cfg.demoted_session_ttl;
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
let mut hs_removed = 0u64;
|
||||
let mut ss_removed = 0u64;
|
||||
let mut demoted_removed = 0u64;
|
||||
|
||||
// Remove stale handshakes (based on age since creation)
|
||||
handshake_states.retain(|_, timestamped| {
|
||||
if timestamped.age() > handshake_ttl {
|
||||
hs_removed += 1;
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
// Remove stale sessions (based on time since last activity)
|
||||
// Use shorter TTL for demoted (ReadOnlyTransport) sessions
|
||||
session_states.retain(|_, timestamped| {
|
||||
@@ -651,17 +622,14 @@ pub(crate) mod cleanup_task {
|
||||
}
|
||||
});
|
||||
|
||||
if hs_removed > 0 || ss_removed > 0 || demoted_removed > 0 {
|
||||
if ss_removed > 0 || demoted_removed > 0 {
|
||||
let duration = start.elapsed();
|
||||
info!(
|
||||
"LP state cleanup: removed {hs_removed} handshakes, {ss_removed} sessions, {demoted_removed} demoted (took {:.3}s)",
|
||||
"LP state cleanup: {ss_removed} sessions, {demoted_removed} demoted (took {:.3}s)",
|
||||
duration.as_secs_f64()
|
||||
);
|
||||
|
||||
// Track metrics
|
||||
if hs_removed > 0 {
|
||||
inc_by!("lp_states_cleanup_handshake_removed", hs_removed as i64);
|
||||
}
|
||||
if ss_removed > 0 {
|
||||
inc_by!("lp_states_cleanup_session_removed", ss_removed as i64);
|
||||
}
|
||||
@@ -679,7 +647,6 @@ pub(crate) mod cleanup_task {
|
||||
/// Demoted sessions (ReadOnlyTransport) use shorter TTL since they
|
||||
/// only need to drain in-flight packets after subsession promotion.
|
||||
pub(crate) async fn cleanup_loop(
|
||||
handshake_states: Arc<DashMap<u32, TimestampedState<LpStateMachine>>>,
|
||||
session_states: Arc<DashMap<u32, TimestampedState<LpStateMachine>>>,
|
||||
cfg: LpDebug,
|
||||
shutdown: nym_task::ShutdownToken,
|
||||
@@ -697,7 +664,7 @@ pub(crate) mod cleanup_task {
|
||||
break;
|
||||
}
|
||||
_ = cleanup_interval.tick() => {
|
||||
perform_cleanup(&handshake_states, &session_states, cfg).await;
|
||||
perform_cleanup(&session_states, cfg).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,7 +362,6 @@ impl GatewayTasksBuilder {
|
||||
peer_registrator,
|
||||
lp_config: self.config.lp,
|
||||
outbound_mix_sender: self.mix_packet_sender.clone(),
|
||||
handshake_states: Arc::new(dashmap::DashMap::new()),
|
||||
session_states: Arc::new(dashmap::DashMap::new()),
|
||||
forward_semaphore: Arc::new(Semaphore::new(
|
||||
self.config.lp.debug.max_concurrent_forwards,
|
||||
|
||||
@@ -237,9 +237,6 @@ mod tests {
|
||||
// TODO: might be needed later on for mixnet registration
|
||||
outbound_mix_sender: mix_sender,
|
||||
|
||||
// we start with empty state
|
||||
handshake_states: Arc::new(Default::default()),
|
||||
|
||||
// we start with empty state
|
||||
session_states: Arc::new(Default::default()),
|
||||
|
||||
|
||||
@@ -8,19 +8,18 @@ use super::error::{LpClientError, Result};
|
||||
use crate::lp_client::helpers::{
|
||||
LpDataDeliverExt, LpDataSendExt, convert_forward_data, try_convert_forward_response,
|
||||
};
|
||||
use crate::lp_client::state_machine_helpers::{
|
||||
extract_forwarded_response, get_recv_key, get_send_key, prepare_send_packet,
|
||||
};
|
||||
use crate::lp_client::nested_session::connection::NestedConnection;
|
||||
use crate::lp_client::state_machine_helpers::{extract_forwarded_response, prepare_send_packet};
|
||||
use bytes::BytesMut;
|
||||
use nym_bandwidth_controller::{BandwidthTicketProvider, DEFAULT_TICKETS_TO_SPEND};
|
||||
use nym_credentials_interface::TicketType;
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_lp::LpPacket;
|
||||
use nym_lp::codec::{OuterAeadKey, parse_lp_packet, serialize_lp_packet};
|
||||
use nym_lp::message::ForwardPacketData;
|
||||
use nym_lp::packet::version;
|
||||
use nym_lp::peer::{LpLocalPeer, LpRemotePeer};
|
||||
use nym_lp::state_machine::{LpAction, LpData, LpInput, LpStateMachine};
|
||||
use nym_lp::{LpPacket, LpSession};
|
||||
use nym_lp_transport::traits::LpTransport;
|
||||
use nym_registration_common::dvpn::LpDvpnRegistrationResponseMessageContent;
|
||||
use nym_registration_common::{
|
||||
@@ -31,7 +30,7 @@ use nym_wireguard_types::PeerPublicKey;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use std::time::Duration;
|
||||
use tokio::net::TcpStream;
|
||||
use tracing::warn;
|
||||
|
||||
@@ -67,7 +66,7 @@ pub struct LpRegistrationClient<S = TcpStream> {
|
||||
state_machine: Option<LpStateMachine>,
|
||||
|
||||
/// Configuration for timeouts and TCP parameters.
|
||||
config: LpRegistrationConfig,
|
||||
pub(crate) config: LpRegistrationConfig,
|
||||
|
||||
/// Persistent TCP stream for the connection.
|
||||
/// Opened on first use, closed after registration.
|
||||
@@ -119,6 +118,19 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to use this `LpRegistrationClient` as transport for `NestedSession`
|
||||
pub fn as_nested_connection(
|
||||
&mut self,
|
||||
exit_identity: ed25519::PublicKey,
|
||||
exit_address: SocketAddr,
|
||||
) -> NestedConnection<'_, S> {
|
||||
NestedConnection {
|
||||
exit_identity,
|
||||
exit_address,
|
||||
outer_client: self,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new LP registration client with default configuration.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -145,15 +157,7 @@ where
|
||||
)
|
||||
}
|
||||
|
||||
fn state_machine(&self) -> Result<&LpStateMachine> {
|
||||
self.state_machine.as_ref().ok_or_else(|| {
|
||||
LpClientError::transport(
|
||||
"State machine not available - has the handshake been completed?",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn state_machine_mut(&mut self) -> Result<&mut LpStateMachine> {
|
||||
pub(crate) fn state_machine_mut(&mut self) -> Result<&mut LpStateMachine> {
|
||||
self.state_machine.as_mut().ok_or_else(|| {
|
||||
LpClientError::transport(
|
||||
"State machine not available - has the handshake been completed?",
|
||||
@@ -169,11 +173,7 @@ where
|
||||
|
||||
/// Returns whether the client has completed the handshake and is ready for registration.
|
||||
pub fn is_handshake_complete(&self) -> bool {
|
||||
self.state_machine
|
||||
.as_ref()
|
||||
.and_then(|sm| sm.session().ok())
|
||||
.map(|s| s.is_handshake_complete())
|
||||
.unwrap_or(false)
|
||||
self.state_machine.is_some()
|
||||
}
|
||||
|
||||
/// Returns the gateway LP address this client is configured for.
|
||||
@@ -287,27 +287,20 @@ where
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if not connected or if send fails.
|
||||
async fn try_send_packet(&mut self, packet: &LpPacket) -> Result<()> {
|
||||
let state_machine = self.state_machine()?;
|
||||
let send_key = get_send_key(state_machine);
|
||||
self.try_send_packet_with_key(packet, send_key.as_ref())
|
||||
.await
|
||||
}
|
||||
pub(crate) async fn try_send_packet(&mut self, packet: &LpPacket) -> Result<()> {
|
||||
// can't use getters due to borrow checker (i.e. requiring full borrows for function calls)
|
||||
let stream = self
|
||||
.stream
|
||||
.as_mut()
|
||||
.ok_or_else(|| LpClientError::transport("Cannot send: not connected"))?;
|
||||
|
||||
/// Sends an LP packet (and optionally encrypted) on the persistent stream.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `packet` - The LP packet to send
|
||||
/// * `outer_key` - Optional outer AEAD key for encryption
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if not connected or if send fails.
|
||||
async fn try_send_packet_with_key(
|
||||
&mut self,
|
||||
packet: &LpPacket,
|
||||
outer_key: Option<&OuterAeadKey>,
|
||||
) -> Result<()> {
|
||||
let stream = self.stream_mut()?;
|
||||
let state_machine = self.state_machine.as_ref().ok_or_else(|| {
|
||||
LpClientError::transport(
|
||||
"State machine not available - has the handshake been completed?",
|
||||
)
|
||||
})?;
|
||||
|
||||
let outer_key = state_machine.session()?.outer_aead_key();
|
||||
Self::send_packet_with_key(stream, packet, outer_key).await
|
||||
}
|
||||
|
||||
@@ -315,25 +308,19 @@ where
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if not connected or if receive fails.
|
||||
async fn try_receive_packet(&mut self) -> Result<LpPacket> {
|
||||
let state_machine = self.state_machine()?;
|
||||
let recv_key = get_recv_key(state_machine);
|
||||
pub(crate) async fn try_receive_packet(&mut self) -> Result<LpPacket> {
|
||||
let stream = self
|
||||
.stream
|
||||
.as_mut()
|
||||
.ok_or_else(|| LpClientError::transport("Cannot send: not connected"))?;
|
||||
|
||||
self.try_receive_packet_with_key(recv_key.as_ref()).await
|
||||
}
|
||||
let state_machine = self.state_machine.as_ref().ok_or_else(|| {
|
||||
LpClientError::transport(
|
||||
"State machine not available - has the handshake been completed?",
|
||||
)
|
||||
})?;
|
||||
|
||||
/// Receives an LP packet from the persistent stream.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `outer_key` - Optional outer AEAD key for decryption
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if not connected or if receive fails.
|
||||
async fn try_receive_packet_with_key(
|
||||
&mut self,
|
||||
outer_key: Option<&OuterAeadKey>,
|
||||
) -> Result<LpPacket> {
|
||||
let stream = self.stream_mut()?;
|
||||
let outer_key = state_machine.session()?.outer_aead_key();
|
||||
|
||||
Self::receive_packet_with_key(stream, outer_key).await
|
||||
}
|
||||
@@ -420,256 +407,28 @@ where
|
||||
// Ensure we have a TCP connection
|
||||
self.ensure_connected().await?;
|
||||
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|_| LpClientError::Other("System time before UNIX epoch".into()))?
|
||||
.as_secs();
|
||||
let local_peer = self.lp_local_peer.clone();
|
||||
let remote_peer = self.gateway_lp_peer.clone();
|
||||
let protocol_version = self.gateway_supported_lp_protocol_version;
|
||||
let connection = self.stream_mut()?;
|
||||
|
||||
// Step 1: Generate ClientHelloData with fresh salt and both public keys
|
||||
let client_hello_data = self.lp_local_peer.build_client_hello_data(timestamp);
|
||||
let salt = client_hello_data.salt;
|
||||
let receiver_index = client_hello_data.receiver_index;
|
||||
|
||||
tracing::trace!(
|
||||
"Generated ClientHello with timestamp: {}, receiver_index: {}",
|
||||
client_hello_data.extract_timestamp(),
|
||||
receiver_index
|
||||
);
|
||||
|
||||
// Step 2: Send ClientHello and receive Ack (persistent connection)
|
||||
let client_hello_header = nym_lp::packet::LpHeader::new(
|
||||
nym_lp::BOOTSTRAP_RECEIVER_IDX, // session_id not yet established
|
||||
0, // counter starts at 0
|
||||
self.gateway_supported_lp_protocol_version,
|
||||
);
|
||||
let client_hello_packet = nym_lp::LpPacket::new(
|
||||
client_hello_header,
|
||||
nym_lp::LpMessage::ClientHello(client_hello_data),
|
||||
);
|
||||
|
||||
// Send ClientHello (no outer key - before PSK)
|
||||
self.try_send_packet_with_key(&client_hello_packet, None)
|
||||
.await?;
|
||||
// Receive Ack (no outer key - before PSK)
|
||||
let ack_response = self.try_receive_packet_with_key(None).await?;
|
||||
|
||||
// Verify we received Ack
|
||||
// this confirms that gateway is fine with our suggested protocol version
|
||||
// in the future we probably have some fancier negotiation
|
||||
match ack_response.message() {
|
||||
nym_lp::LpMessage::Ack => {
|
||||
tracing::debug!("Received Ack for ClientHello");
|
||||
}
|
||||
other => {
|
||||
return Err(LpClientError::Transport(format!(
|
||||
"Expected Ack for ClientHello, got: {:?}",
|
||||
other
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Create state machine as initiator with Ed25519 keys
|
||||
// PSK derivation happens internally in the state machine constructor
|
||||
let mut state_machine = LpStateMachine::new(
|
||||
receiver_index,
|
||||
true, // is_initiator
|
||||
self.lp_local_peer.clone(),
|
||||
self.gateway_lp_peer.clone(),
|
||||
&salt,
|
||||
self.gateway_supported_lp_protocol_version,
|
||||
)?;
|
||||
|
||||
// Step 4: Start handshake - get first packet to send (KKT request)
|
||||
let mut pending_packet: Option<nym_lp::LpPacket> = None;
|
||||
if let Some(action) = state_machine.process_input(LpInput::StartHandshake) {
|
||||
match action? {
|
||||
LpAction::SendPacket(packet) => {
|
||||
pending_packet = Some(packet);
|
||||
}
|
||||
other => {
|
||||
return Err(LpClientError::Transport(format!(
|
||||
"Unexpected action at handshake start: {:?}",
|
||||
other
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Handshake loop - all packets on persistent connection
|
||||
loop {
|
||||
// Send pending packet if we have one
|
||||
if let Some(packet) = pending_packet.take() {
|
||||
// Get outer keys from session:
|
||||
// - send_key: outer_aead_key_for_sending() returns None until PSQ complete
|
||||
// - recv_key: outer_aead_key() returns key as soon as PSK is derived
|
||||
let send_key = state_machine
|
||||
.session()
|
||||
.ok()
|
||||
.and_then(|s| s.outer_aead_key_for_sending());
|
||||
let recv_key = state_machine
|
||||
.session()
|
||||
.ok()
|
||||
.and_then(|s| s.outer_aead_key());
|
||||
|
||||
tracing::trace!(
|
||||
"Sending handshake packet (send_key={}, recv_key={})",
|
||||
send_key.is_some(),
|
||||
recv_key.is_some()
|
||||
);
|
||||
self.try_send_packet_with_key(&packet, send_key.as_ref())
|
||||
.await?;
|
||||
let response = self.try_receive_packet_with_key(recv_key.as_ref()).await?;
|
||||
tracing::trace!("Received handshake response");
|
||||
|
||||
// Process the received packet
|
||||
if let Some(action) = state_machine.process_input(LpInput::ReceivePacket(response))
|
||||
{
|
||||
match action? {
|
||||
LpAction::SendPacket(response_packet) => {
|
||||
// Queue the response packet to send on next iteration
|
||||
pending_packet = Some(response_packet);
|
||||
|
||||
// Check if handshake completed after queueing this packet
|
||||
if state_machine.session()?.is_handshake_complete() {
|
||||
// Send the final packet before breaking
|
||||
if let Some(final_packet) = pending_packet.take() {
|
||||
let send_key = state_machine
|
||||
.session()
|
||||
.ok()
|
||||
.and_then(|s| s.outer_aead_key_for_sending());
|
||||
let recv_key = state_machine
|
||||
.session()
|
||||
.ok()
|
||||
.and_then(|s| s.outer_aead_key());
|
||||
tracing::trace!("Sending final handshake packet");
|
||||
self.try_send_packet_with_key(&final_packet, send_key.as_ref())
|
||||
.await?;
|
||||
let ack_response =
|
||||
self.try_receive_packet_with_key(recv_key.as_ref()).await?;
|
||||
|
||||
// Validate Ack response
|
||||
match ack_response.message() {
|
||||
nym_lp::LpMessage::Ack => {
|
||||
tracing::debug!(
|
||||
"Received Ack for final handshake packet"
|
||||
);
|
||||
}
|
||||
other => {
|
||||
return Err(LpClientError::Transport(format!(
|
||||
"Expected Ack for final handshake packet, got: {:?}",
|
||||
other
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
tracing::info!("LP handshake completed after sending final packet");
|
||||
break;
|
||||
}
|
||||
}
|
||||
LpAction::HandshakeComplete => {
|
||||
tracing::info!("LP handshake completed successfully");
|
||||
break;
|
||||
}
|
||||
LpAction::KKTComplete => {
|
||||
tracing::info!("KKT exchange completed, starting Noise handshake");
|
||||
// After KKT completes, initiator must send first Noise handshake message
|
||||
let noise_msg = state_machine
|
||||
.session()?
|
||||
.prepare_handshake_message()
|
||||
.ok_or_else(|| {
|
||||
LpClientError::transport("No handshake message available after KKT")
|
||||
})??;
|
||||
let noise_packet = state_machine.session()?.next_packet(noise_msg)?;
|
||||
pending_packet = Some(noise_packet);
|
||||
}
|
||||
other => {
|
||||
tracing::trace!("Received action during handshake: {:?}", other);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No pending packet and not complete - something is wrong
|
||||
return Err(LpClientError::transport(
|
||||
"Handshake stalled: no packet to send",
|
||||
));
|
||||
}
|
||||
}
|
||||
// TODO:
|
||||
let ciphersuite = LpSession::default_ciphersuite();
|
||||
let session = LpSession::complete_as_initiator(
|
||||
connection,
|
||||
ciphersuite,
|
||||
local_peer,
|
||||
remote_peer,
|
||||
protocol_version,
|
||||
)
|
||||
.complete_as_initiator()
|
||||
.await?;
|
||||
|
||||
// Store the state machine (with established session) for later use
|
||||
self.state_machine = Some(state_machine);
|
||||
self.state_machine = Some(LpStateMachine::new(session));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Opens a TCP connection, sends one packet, receives one response, closes.
|
||||
///
|
||||
/// This implements the packet-per-connection model where each LP packet
|
||||
/// exchange uses its own TCP connection. The connection is closed when
|
||||
/// this method returns (stream dropped).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `address` - Gateway LP listener address
|
||||
/// * `packet` - The LP packet to send
|
||||
/// * `outer_key` - Optional outer AEAD key (None before PSK, Some after)
|
||||
/// * `config` - Configuration for timeouts and TCP parameters
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if connection, send, or receive fails.
|
||||
///
|
||||
/// # Outer AEAD Keys
|
||||
///
|
||||
/// Send and receive use separate keys because during the PSQ handshake:
|
||||
/// - Initiator derives PSK when preparing msg 1, but must send it cleartext
|
||||
/// (responder hasn't derived PSK yet)
|
||||
/// - Responder sends msg 2 encrypted (both have PSK now)
|
||||
/// - Initiator can decrypt msg 2 (has had PSK since preparing msg 1)
|
||||
///
|
||||
/// Use `outer_aead_key_for_sending()` for `send_key` (gates on PSQ completion)
|
||||
/// and `outer_aead_key()` for `recv_key` (available as soon as PSK derived).
|
||||
///
|
||||
/// # Note
|
||||
/// This method is kept for reference but is no longer used. The persistent
|
||||
/// connection model uses `send_packet()` and `receive_packet()` instead.
|
||||
#[allow(dead_code)]
|
||||
async fn connect_send_receive(
|
||||
address: SocketAddr,
|
||||
packet: &LpPacket,
|
||||
send_key: Option<&OuterAeadKey>,
|
||||
recv_key: Option<&OuterAeadKey>,
|
||||
config: &LpRegistrationConfig,
|
||||
) -> Result<LpPacket> {
|
||||
// 1. Connect with timeout
|
||||
let mut stream = tokio::time::timeout(config.connect_timeout, S::connect(address))
|
||||
.await
|
||||
.map_err(|_| LpClientError::TcpConnection {
|
||||
address: address.to_string(),
|
||||
source: std::io::Error::new(
|
||||
std::io::ErrorKind::TimedOut,
|
||||
format!("Connection timeout after {:?}", config.connect_timeout),
|
||||
),
|
||||
})?
|
||||
.map_err(|source| LpClientError::TcpConnection {
|
||||
address: address.to_string(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
// 2. Set TCP_NODELAY
|
||||
stream
|
||||
.set_no_delay(config.tcp_nodelay)
|
||||
.map_err(|source| LpClientError::TcpConnection {
|
||||
address: address.to_string(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
// 3. Send packet with send_key
|
||||
Self::send_packet_with_key(&mut stream, packet, send_key).await?;
|
||||
|
||||
// 4. Receive response with recv_key
|
||||
let response = Self::receive_packet_with_key(&mut stream, recv_key).await?;
|
||||
|
||||
// Connection drops when stream goes out of scope
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Sends an LP packet over a TCP stream with length-prefixed framing.
|
||||
///
|
||||
/// Format: 4-byte big-endian u32 length + packet bytes
|
||||
@@ -684,10 +443,10 @@ where
|
||||
async fn send_packet_with_key(
|
||||
stream: &mut S,
|
||||
packet: &LpPacket,
|
||||
outer_key: Option<&OuterAeadKey>,
|
||||
outer_key: &OuterAeadKey,
|
||||
) -> Result<()> {
|
||||
let mut packet_buf = BytesMut::new();
|
||||
serialize_lp_packet(packet, &mut packet_buf, outer_key)
|
||||
serialize_lp_packet(packet, &mut packet_buf, Some(outer_key))
|
||||
.map_err(|e| LpClientError::Transport(format!("Failed to serialize packet: {e}")))?;
|
||||
|
||||
stream
|
||||
@@ -709,16 +468,13 @@ where
|
||||
/// - Network read fails
|
||||
/// - Packet size exceeds maximum (64KB)
|
||||
/// - Packet parsing/decryption fails
|
||||
async fn receive_packet_with_key(
|
||||
stream: &mut S,
|
||||
outer_key: Option<&OuterAeadKey>,
|
||||
) -> Result<LpPacket> {
|
||||
async fn receive_packet_with_key(stream: &mut S, outer_key: &OuterAeadKey) -> Result<LpPacket> {
|
||||
let packet_buf = stream
|
||||
.receive_raw_packet()
|
||||
.await
|
||||
.map_err(|err| LpClientError::transport(err.to_string()))?;
|
||||
|
||||
let packet = parse_lp_packet(&packet_buf, outer_key)
|
||||
let packet = parse_lp_packet(&packet_buf, Some(outer_key))
|
||||
.map_err(|e| LpClientError::Transport(format!("Failed to parse packet: {e}")))?;
|
||||
|
||||
Ok(packet)
|
||||
@@ -1061,47 +817,30 @@ where
|
||||
// 1. Serialize the ForwardPacketData
|
||||
let input = convert_forward_data(forward_data)?;
|
||||
|
||||
// 2. Encrypt and prepare packet via state machine (scoped borrow)
|
||||
let (forward_packet, send_key, recv_key) = {
|
||||
let state_machine = self.state_machine.as_mut().ok_or_else(|| {
|
||||
LpClientError::transport("Cannot send forward packet: handshake not completed")
|
||||
// 2. Encrypt and prepare packet via state machine
|
||||
let state_machine = self.state_machine_mut()?;
|
||||
|
||||
let action = state_machine
|
||||
.process_input(input)
|
||||
.ok_or_else(|| LpClientError::transport("State machine returned no action"))?
|
||||
.map_err(|e| {
|
||||
LpClientError::Transport(format!("Failed to encrypt ForwardPacket: {e}"))
|
||||
})?;
|
||||
|
||||
let action = state_machine
|
||||
.process_input(input)
|
||||
.ok_or_else(|| LpClientError::transport("State machine returned no action"))?
|
||||
.map_err(|e| {
|
||||
LpClientError::Transport(format!("Failed to encrypt ForwardPacket: {e}"))
|
||||
})?;
|
||||
|
||||
let forward_packet = match action {
|
||||
LpAction::SendPacket(packet) => packet,
|
||||
other => {
|
||||
return Err(LpClientError::Transport(format!(
|
||||
"Unexpected action when sending ForwardPacket: {:?}",
|
||||
other
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
// Get outer keys from session
|
||||
let send_key = state_machine
|
||||
.session()
|
||||
.ok()
|
||||
.and_then(|s| s.outer_aead_key_for_sending());
|
||||
let recv_key = state_machine
|
||||
.session()
|
||||
.ok()
|
||||
.and_then(|s| s.outer_aead_key());
|
||||
|
||||
(forward_packet, send_key, recv_key)
|
||||
}; // state_machine borrow ends here
|
||||
let forward_packet = match action {
|
||||
LpAction::SendPacket(packet) => packet,
|
||||
other => {
|
||||
return Err(LpClientError::Transport(format!(
|
||||
"Unexpected action when sending ForwardPacket: {:?}",
|
||||
other
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
// 3. Send and receive on persistent connection with timeout
|
||||
let response_packet = tokio::time::timeout(self.config.forward_timeout, async {
|
||||
self.try_send_packet_with_key(&forward_packet, send_key.as_ref())
|
||||
.await?;
|
||||
self.try_receive_packet_with_key(recv_key.as_ref()).await
|
||||
self.try_send_packet(&forward_packet).await?;
|
||||
self.try_receive_packet().await
|
||||
})
|
||||
.await
|
||||
.map_err(|_| {
|
||||
@@ -1185,14 +924,11 @@ where
|
||||
};
|
||||
|
||||
// Get outer AEAD key for encryption
|
||||
let outer_key = state_machine
|
||||
.session()
|
||||
.ok()
|
||||
.and_then(|s| s.outer_aead_key_for_sending());
|
||||
let outer_key = state_machine.session()?.outer_aead_key();
|
||||
|
||||
// Serialize the packet with outer AEAD encryption
|
||||
let mut buf = BytesMut::new();
|
||||
serialize_lp_packet(&packet, &mut buf, outer_key.as_ref())
|
||||
serialize_lp_packet(&packet, &mut buf, Some(outer_key))
|
||||
.map_err(|e| LpClientError::Transport(format!("Failed to serialize LP packet: {e}")))?;
|
||||
|
||||
Ok(buf.to_vec())
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::lp_client::helpers::{convert_forward_data, try_convert_forward_response};
|
||||
use crate::{LpClientError, LpRegistrationClient};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_lp::message::ForwardPacketData;
|
||||
use nym_lp::state_machine::{LpAction, LpInput};
|
||||
use nym_lp_transport::traits::LpTransport;
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
/// Attempt to treat the inner client as a LP connection
|
||||
pub struct NestedConnection<'a, S> {
|
||||
/// Remote Ed25519 public key
|
||||
pub(crate) exit_identity: ed25519::PublicKey,
|
||||
|
||||
/// Exit gateway's LP address (e.g., "2.2.2.2:41264")
|
||||
pub(crate) exit_address: SocketAddr,
|
||||
|
||||
pub(crate) outer_client: &'a mut LpRegistrationClient<S>,
|
||||
}
|
||||
|
||||
impl<'a, S> NestedConnection<'a, S> {
|
||||
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> Result<(), LpClientError>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
let forward_packet_data =
|
||||
ForwardPacketData::new(self.exit_identity, self.exit_address, packet_data.to_vec());
|
||||
|
||||
let target_address = self.exit_address;
|
||||
|
||||
tracing::debug!(
|
||||
"Sending ForwardPacket to {target_address} ({} inner bytes, persistent connection)",
|
||||
forward_packet_data.inner_packet_bytes.len()
|
||||
);
|
||||
|
||||
// 1. Serialize the ForwardPacketData
|
||||
let input = convert_forward_data(forward_packet_data)?;
|
||||
|
||||
// 2. Encrypt and prepare packet via state machine
|
||||
let state_machine = self.outer_client.state_machine_mut()?;
|
||||
|
||||
let action = state_machine
|
||||
.process_input(input)
|
||||
.ok_or_else(|| LpClientError::transport("State machine returned no action"))?
|
||||
.map_err(|e| {
|
||||
LpClientError::Transport(format!("Failed to encrypt ForwardPacket: {e}"))
|
||||
})?;
|
||||
|
||||
let forward_packet = match action {
|
||||
LpAction::SendPacket(packet) => packet,
|
||||
other => {
|
||||
return Err(LpClientError::Transport(format!(
|
||||
"Unexpected action when sending ForwardPacket: {:?}",
|
||||
other
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
// 3. Send the packet with timeout
|
||||
let timeout = self.outer_client.config.forward_timeout;
|
||||
tokio::time::timeout(timeout, async {
|
||||
self.outer_client.try_send_packet(&forward_packet).await
|
||||
})
|
||||
.await
|
||||
.map_err(|_| {
|
||||
LpClientError::Transport(format!("Forward packet timeout after {timeout:?}",))
|
||||
})??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn receive_raw_packet(&mut self) -> Result<Vec<u8>, LpClientError>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
// 1. Receive the packet with timeout
|
||||
let timeout = self.outer_client.config.forward_timeout;
|
||||
let response_packet = tokio::time::timeout(timeout, async {
|
||||
self.outer_client.try_receive_packet().await
|
||||
})
|
||||
.await
|
||||
.map_err(|_| {
|
||||
LpClientError::Transport(format!("Forward packet timeout after {timeout:?}",))
|
||||
})??;
|
||||
|
||||
// 2. Decrypt via state machine (re-borrow)
|
||||
let state_machine = self.outer_client.state_machine_mut()?;
|
||||
let action = state_machine
|
||||
.process_input(LpInput::ReceivePacket(response_packet))
|
||||
.ok_or_else(|| LpClientError::transport("State machine returned no action"))?
|
||||
.map_err(|e| {
|
||||
LpClientError::Transport(format!("Failed to decrypt forward response: {e}"))
|
||||
})?;
|
||||
|
||||
// 3. Extract decrypted response data
|
||||
let response_data = try_convert_forward_response(action)?;
|
||||
|
||||
tracing::debug!(
|
||||
"Successfully received forward response from {} ({} bytes)",
|
||||
self.exit_address,
|
||||
response_data.len()
|
||||
);
|
||||
|
||||
Ok(response_data)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, S> LpTransport for NestedConnection<'a, S>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
#[allow(clippy::unimplemented)]
|
||||
async fn connect(_: SocketAddr) -> std::io::Result<Self> {
|
||||
// this really breaks the pattern and should be refactored
|
||||
// since this function should never be called
|
||||
unimplemented!("cannot establish nested connection without an outer client")
|
||||
}
|
||||
|
||||
fn set_no_delay(&mut self, _: bool) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_serialised_packet(&mut self, packet_data: &[u8]) -> std::io::Result<()> {
|
||||
self.send_serialised_packet(packet_data)
|
||||
.await
|
||||
.map_err(io::Error::other)
|
||||
}
|
||||
|
||||
async fn receive_raw_packet(&mut self) -> std::io::Result<Vec<u8>> {
|
||||
self.receive_raw_packet().await.map_err(io::Error::other)
|
||||
}
|
||||
}
|
||||
+23
-182
@@ -22,8 +22,7 @@ use super::client::LpRegistrationClient;
|
||||
use super::error::{LpClientError, Result};
|
||||
use crate::lp_client::helpers::{LpDataDeliverExt, LpDataSendExt};
|
||||
use crate::lp_client::state_machine_helpers::{
|
||||
extract_forwarded_response, get_recv_key, get_send_key, prepare_serialised_send_packet,
|
||||
serialize_packet,
|
||||
extract_forwarded_response, prepare_serialised_send_packet,
|
||||
};
|
||||
use nym_bandwidth_controller::{BandwidthTicketProvider, DEFAULT_TICKETS_TO_SPEND};
|
||||
use nym_credentials_interface::TicketType;
|
||||
@@ -32,8 +31,8 @@ use nym_lp::codec::{OuterAeadKey, parse_lp_packet};
|
||||
use nym_lp::message::ForwardPacketData;
|
||||
use nym_lp::packet::version;
|
||||
use nym_lp::peer::{LpLocalPeer, LpRemotePeer};
|
||||
use nym_lp::state_machine::{LpAction, LpData, LpInput, LpStateMachine};
|
||||
use nym_lp::{LpMessage, LpPacket};
|
||||
use nym_lp::state_machine::{LpData, LpStateMachine};
|
||||
use nym_lp::{LpPacket, LpSession};
|
||||
use nym_lp_transport::traits::LpTransport;
|
||||
use nym_registration_common::dvpn::LpDvpnRegistrationResponseMessageContent;
|
||||
use nym_registration_common::{
|
||||
@@ -44,9 +43,10 @@ use nym_wireguard_types::PeerPublicKey;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tracing::warn;
|
||||
|
||||
pub(crate) mod connection;
|
||||
|
||||
/// Manages a nested LP session where the client establishes a handshake with
|
||||
/// an exit gateway by forwarding packets through an entry gateway.
|
||||
///
|
||||
@@ -140,8 +140,8 @@ impl NestedLpSession {
|
||||
/// Attempt to parse received bytes into an LpPacket
|
||||
fn parse_received_lp_packet(&self, response_bytes: Vec<u8>) -> Result<LpPacket> {
|
||||
let state_machine = self.state_machine()?;
|
||||
let outer_key = get_recv_key(state_machine);
|
||||
Self::parse_packet(&response_bytes, outer_key.as_ref())
|
||||
let outer_key = state_machine.session()?.outer_aead_key();
|
||||
Self::parse_packet(&response_bytes, Some(outer_key))
|
||||
}
|
||||
|
||||
/// Attempt to wrap the provided `LpData` into a `ForwardPacketData`
|
||||
@@ -193,155 +193,26 @@ impl NestedLpSession {
|
||||
self.exit_address
|
||||
);
|
||||
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|_| LpClientError::Other("System time before UNIX epoch".into()))?
|
||||
.as_secs();
|
||||
let mut nested_connection =
|
||||
outer_client.as_nested_connection(self.gateway_lp_peer.ed25519(), self.exit_address);
|
||||
|
||||
// Step 1: Generate ClientHello for exit gateway
|
||||
let client_hello_data = self.lp_local_peer.build_client_hello_data(timestamp);
|
||||
let salt = client_hello_data.salt;
|
||||
let receiver_index = client_hello_data.receiver_index;
|
||||
let local_peer = self.lp_local_peer.clone();
|
||||
let remote_peer = self.gateway_lp_peer.clone();
|
||||
let protocol_version = self.gateway_supported_lp_protocol_version;
|
||||
|
||||
tracing::trace!(
|
||||
"Generated ClientHello for exit gateway (timestamp: {})",
|
||||
client_hello_data.extract_timestamp()
|
||||
);
|
||||
|
||||
// Step 2: Send ClientHello to exit gateway via forwarding
|
||||
let client_hello_header = nym_lp::packet::LpHeader::new(
|
||||
nym_lp::BOOTSTRAP_RECEIVER_IDX, // Use constant for bootstrap session
|
||||
0, // counter starts at 0
|
||||
self.gateway_supported_lp_protocol_version,
|
||||
);
|
||||
let client_hello_packet = nym_lp::LpPacket::new(
|
||||
client_hello_header,
|
||||
LpMessage::ClientHello(client_hello_data),
|
||||
);
|
||||
|
||||
// Serialize and forward ClientHello (no state machine yet, no outer key)
|
||||
let client_hello_bytes = serialize_packet(&client_hello_packet, None)?;
|
||||
let forward_packet_data = ForwardPacketData::new(
|
||||
self.gateway_lp_peer.ed25519(),
|
||||
self.exit_address,
|
||||
client_hello_bytes,
|
||||
);
|
||||
|
||||
let response_bytes = outer_client
|
||||
.send_forward_packet_with_response(forward_packet_data)
|
||||
.await?;
|
||||
|
||||
// Parse and validate Ack response (cleartext, no outer key before PSK derivation)
|
||||
// this confirms that gateway is fine with our suggested protocol version
|
||||
// in the future we probably have some fancier negotiation
|
||||
let ack_response = Self::parse_packet(&response_bytes, None)?;
|
||||
match ack_response.message() {
|
||||
LpMessage::Ack => {
|
||||
tracing::debug!("Received Ack for ClientHello from exit gateway");
|
||||
}
|
||||
LpMessage::Collision => {
|
||||
return Err(LpClientError::Transport(format!(
|
||||
"Exit gateway returned Collision - receiver_index {receiver_index} already in use",
|
||||
)));
|
||||
}
|
||||
other => {
|
||||
return Err(LpClientError::Transport(format!(
|
||||
"Expected Ack for ClientHello from exit gateway, got: {:?}",
|
||||
other
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Create state machine for exit gateway handshake
|
||||
let mut state_machine = LpStateMachine::new(
|
||||
receiver_index,
|
||||
true, // is_initiator
|
||||
self.lp_local_peer.clone(),
|
||||
self.gateway_lp_peer.clone(),
|
||||
&salt,
|
||||
self.gateway_supported_lp_protocol_version,
|
||||
)?;
|
||||
|
||||
// Step 4: Get initial packet from StartHandshake
|
||||
let mut pending_packet: Option<LpPacket> = None;
|
||||
if let Some(action) = state_machine.process_input(LpInput::StartHandshake) {
|
||||
match action? {
|
||||
LpAction::SendPacket(packet) => {
|
||||
pending_packet = Some(packet);
|
||||
}
|
||||
other => {
|
||||
return Err(LpClientError::Transport(format!(
|
||||
"Unexpected action at handshake start: {other:?}",
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Handshake loop - each packet on new connection via forwarding
|
||||
loop {
|
||||
if let Some(packet) = pending_packet.take() {
|
||||
tracing::trace!("Sending handshake packet to exit via forwarding");
|
||||
let response = self
|
||||
.send_and_receive_via_forward(outer_client, &state_machine, &packet)
|
||||
.await?;
|
||||
tracing::trace!("Received handshake response from exit");
|
||||
|
||||
// Process the received packet
|
||||
if let Some(action) = state_machine.process_input(LpInput::ReceivePacket(response))
|
||||
{
|
||||
match action? {
|
||||
LpAction::SendPacket(response_packet) => {
|
||||
pending_packet = Some(response_packet);
|
||||
|
||||
// Check if handshake completed - send final packet if so
|
||||
if state_machine.session()?.is_handshake_complete() {
|
||||
if let Some(final_packet) = pending_packet.take() {
|
||||
tracing::trace!("Sending final handshake packet to exit");
|
||||
let _ = self
|
||||
.send_and_receive_via_forward(
|
||||
outer_client,
|
||||
&state_machine,
|
||||
&final_packet,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
tracing::info!("Nested LP handshake completed with exit gateway");
|
||||
break;
|
||||
}
|
||||
}
|
||||
LpAction::HandshakeComplete => {
|
||||
tracing::info!("Nested LP handshake completed with exit gateway");
|
||||
break;
|
||||
}
|
||||
LpAction::KKTComplete => {
|
||||
tracing::info!("KKT exchange completed with exit, starting Noise");
|
||||
// After KKT completes, initiator must send first Noise handshake message
|
||||
let noise_msg = state_machine
|
||||
.session()?
|
||||
.prepare_handshake_message()
|
||||
.ok_or_else(|| {
|
||||
LpClientError::Transport(
|
||||
"No handshake message available after KKT".to_string(),
|
||||
)
|
||||
})??;
|
||||
let noise_packet = state_machine.session()?.next_packet(noise_msg)?;
|
||||
pending_packet = Some(noise_packet);
|
||||
}
|
||||
other => {
|
||||
tracing::trace!("Received action during handshake: {:?}", other);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No pending packet and not complete - something is wrong
|
||||
return Err(LpClientError::Transport(
|
||||
"Nested handshake stalled: no packet to send".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
let ciphersuite = LpSession::default_ciphersuite();
|
||||
let session = LpSession::complete_as_initiator(
|
||||
&mut nested_connection,
|
||||
ciphersuite,
|
||||
local_peer,
|
||||
remote_peer,
|
||||
protocol_version,
|
||||
)
|
||||
.complete_as_initiator()
|
||||
.await?;
|
||||
|
||||
// Store the state machine (with established session) for later use
|
||||
self.state_machine = Some(state_machine);
|
||||
self.state_machine = Some(LpStateMachine::new(session));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -660,36 +531,6 @@ impl NestedLpSession {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Sends a packet via forwarding through the entry gateway and returns the parsed response.
|
||||
///
|
||||
/// This helper consolidates the send/receive pattern used throughout the handshake:
|
||||
/// 1. Gets outer AEAD key from state machine (if available)
|
||||
/// 2. Serializes the packet with outer encryption
|
||||
/// 3. Forwards via entry gateway
|
||||
/// 4. Parses and returns the response
|
||||
async fn send_and_receive_via_forward<S>(
|
||||
&self,
|
||||
outer_client: &mut LpRegistrationClient<S>,
|
||||
state_machine: &LpStateMachine,
|
||||
packet: &LpPacket,
|
||||
) -> Result<LpPacket>
|
||||
where
|
||||
S: LpTransport + Unpin,
|
||||
{
|
||||
let send_key = get_send_key(state_machine);
|
||||
let packet_bytes = serialize_packet(packet, send_key.as_ref())?;
|
||||
let forward_data = ForwardPacketData::new(
|
||||
self.gateway_lp_peer.ed25519(),
|
||||
self.exit_address,
|
||||
packet_bytes,
|
||||
);
|
||||
let response_bytes = outer_client
|
||||
.send_forward_packet_with_response(forward_data)
|
||||
.await?;
|
||||
let recv_key = get_recv_key(state_machine);
|
||||
Self::parse_packet(&response_bytes, recv_key.as_ref())
|
||||
}
|
||||
|
||||
/// Parses an LP packet from bytes.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -7,26 +7,6 @@ use nym_lp::codec::{OuterAeadKey, serialize_lp_packet};
|
||||
use nym_lp::state_machine::{LpAction, LpData, LpInput};
|
||||
use nym_lp::{LpPacket, LpStateMachine};
|
||||
|
||||
/// Gets the outer AEAD key for sending (encryption) from the state machine.
|
||||
///
|
||||
/// Returns `None` during early handshake before PSK derivation.
|
||||
pub(crate) fn get_send_key(state_machine: &LpStateMachine) -> Option<OuterAeadKey> {
|
||||
state_machine
|
||||
.session()
|
||||
.ok()
|
||||
.and_then(|s| s.outer_aead_key_for_sending())
|
||||
}
|
||||
|
||||
/// Gets the outer AEAD key for receiving (decryption) from the state machine.
|
||||
///
|
||||
/// Returns `None` during early handshake before PSK derivation.
|
||||
pub(crate) fn get_recv_key(state_machine: &LpStateMachine) -> Option<OuterAeadKey> {
|
||||
state_machine
|
||||
.session()
|
||||
.ok()
|
||||
.and_then(|s| s.outer_aead_key())
|
||||
}
|
||||
|
||||
/// Serializes an LP packet to bytes.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -79,9 +59,9 @@ pub(crate) fn prepare_serialised_send_packet(
|
||||
state_machine: &mut LpStateMachine,
|
||||
) -> Result<Vec<u8>, LpClientError> {
|
||||
let packet = prepare_send_packet(data, state_machine)?;
|
||||
let send_key = state_machine.session()?.outer_aead_key();
|
||||
|
||||
let send_key = get_send_key(state_machine);
|
||||
serialize_packet(&packet, send_key.as_ref())
|
||||
serialize_packet(&packet, Some(send_key))
|
||||
}
|
||||
|
||||
/// Attempt to recover received `LpData` from the received `LpPacket`
|
||||
|
||||
Generated
+33
-164
@@ -81,7 +81,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -106,7 +106,7 @@ dependencies = [
|
||||
"cipher",
|
||||
"ctr",
|
||||
"ghash",
|
||||
"subtle 2.6.1",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -259,7 +259,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2 0.10.6",
|
||||
"blake2",
|
||||
"cpufeatures",
|
||||
"password-hash",
|
||||
]
|
||||
@@ -703,7 +703,7 @@ dependencies = [
|
||||
"ripemd",
|
||||
"secp256k1",
|
||||
"sha2 0.10.9",
|
||||
"subtle 2.6.1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -752,18 +752,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330"
|
||||
dependencies = [
|
||||
"byte-tools",
|
||||
"crypto-mac",
|
||||
"digest 0.8.1",
|
||||
"opaque-debug 0.2.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.10.6"
|
||||
@@ -790,7 +778,7 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
|
||||
dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -799,7 +787,7 @@ version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -876,12 +864,6 @@ version = "3.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||
|
||||
[[package]]
|
||||
name = "byte-tools"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.22.0"
|
||||
@@ -1051,16 +1033,6 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chacha"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddf3c081b5fba1e5615640aae998e0fbd10c24cbd897ee39ed754a77601a4862"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"keystream",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.40"
|
||||
@@ -1467,9 +1439,9 @@ version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
||||
dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.6.1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -1479,21 +1451,11 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
"rand_core 0.6.4",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-mac"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5"
|
||||
dependencies = [
|
||||
"generic-array 0.12.4",
|
||||
"subtle 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cssparser"
|
||||
version = "0.27.2"
|
||||
@@ -1559,7 +1521,7 @@ dependencies = [
|
||||
"fiat-crypto",
|
||||
"rustc_version",
|
||||
"serde",
|
||||
"subtle 2.6.1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -1800,22 +1762,13 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
|
||||
dependencies = [
|
||||
"generic-array 0.12.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
|
||||
dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1827,7 +1780,7 @@ dependencies = [
|
||||
"block-buffer 0.10.4",
|
||||
"const-oid",
|
||||
"crypto-common",
|
||||
"subtle 2.6.1",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2019,7 +1972,7 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"sha2 0.10.9",
|
||||
"subtle 2.6.1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -2054,7 +2007,7 @@ dependencies = [
|
||||
"crypto-bigint",
|
||||
"digest 0.10.7",
|
||||
"ff",
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
"group",
|
||||
"hkdf",
|
||||
"pem-rfc7468",
|
||||
@@ -2062,7 +2015,7 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"sec1",
|
||||
"serdect 0.2.0",
|
||||
"subtle 2.6.1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -2242,7 +2195,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
|
||||
dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.6.1",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2601,15 +2554,6 @@ dependencies = [
|
||||
"windows 0.58.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -2675,7 +2619,7 @@ version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
|
||||
dependencies = [
|
||||
"opaque-debug 0.3.1",
|
||||
"opaque-debug",
|
||||
"polyval",
|
||||
]
|
||||
|
||||
@@ -2789,7 +2733,7 @@ checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
|
||||
dependencies = [
|
||||
"ff",
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.6.1",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3547,7 +3491,7 @@ version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3808,12 +3752,6 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keystream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28"
|
||||
|
||||
[[package]]
|
||||
name = "kuchikiki"
|
||||
version = "0.8.2"
|
||||
@@ -3905,18 +3843,6 @@ version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413"
|
||||
|
||||
[[package]]
|
||||
name = "lioness"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ae926706ba42c425c9457121178330d75e273df2e82e28b758faf3de3a9acb9"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"blake2 0.8.1",
|
||||
"chacha",
|
||||
"keystream",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.7.5"
|
||||
@@ -4336,7 +4262,7 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"serdect 0.3.0",
|
||||
"subtle 2.6.1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -4370,7 +4296,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"sha2 0.10.9",
|
||||
"subtle 2.6.1",
|
||||
"subtle",
|
||||
"thiserror 2.0.12",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -4431,7 +4357,6 @@ dependencies = [
|
||||
"ed25519-dalek",
|
||||
"jwt-simple",
|
||||
"nym-pemstore",
|
||||
"nym-sphinx-types",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
@@ -4681,21 +4606,13 @@ dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-sphinx-types"
|
||||
version = "1.20.4"
|
||||
dependencies = [
|
||||
"sphinx-packet",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-store-cipher"
|
||||
version = "1.20.4"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"argon2",
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
"getrandom 0.2.15",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
@@ -5113,12 +5030,6 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.1"
|
||||
@@ -5322,7 +5233,7 @@ checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"rand_core 0.6.4",
|
||||
"subtle 2.6.1",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5698,7 +5609,7 @@ checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"opaque-debug 0.3.1",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
]
|
||||
|
||||
@@ -6031,16 +5942,6 @@ dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_distr"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_hc"
|
||||
version = "0.2.0"
|
||||
@@ -6290,7 +6191,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
"subtle 2.6.1",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6364,7 +6265,7 @@ dependencies = [
|
||||
"sha2 0.10.9",
|
||||
"signature",
|
||||
"spki",
|
||||
"subtle 2.6.1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -6439,7 +6340,7 @@ dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.103.9",
|
||||
"subtle 2.6.1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -6631,10 +6532,10 @@ checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"der",
|
||||
"generic-array 0.14.7",
|
||||
"generic-array",
|
||||
"pkcs8",
|
||||
"serdect 0.2.0",
|
||||
"subtle 2.6.1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -6939,7 +6840,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest 0.9.0",
|
||||
"opaque-debug 0.3.1",
|
||||
"opaque-debug",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7098,32 +6999,6 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinx-packet"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c26f0c20d909fdda1c5d0ece3973127ca421984d55b000215df365e93722fc6e"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"arrayref",
|
||||
"blake2 0.8.1",
|
||||
"bs58",
|
||||
"byteorder",
|
||||
"chacha",
|
||||
"ctr",
|
||||
"curve25519-dalek",
|
||||
"digest 0.10.7",
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"lioness",
|
||||
"rand 0.8.5",
|
||||
"rand_distr",
|
||||
"sha2 0.10.9",
|
||||
"subtle 2.6.1",
|
||||
"x25519-dalek",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
@@ -7204,12 +7079,6 @@ dependencies = [
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@@ -7798,7 +7667,7 @@ dependencies = [
|
||||
"serde_repr",
|
||||
"sha2 0.10.9",
|
||||
"signature",
|
||||
"subtle 2.6.1",
|
||||
"subtle",
|
||||
"subtle-encoding",
|
||||
"tendermint-proto",
|
||||
"time",
|
||||
@@ -7853,7 +7722,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"serde_json",
|
||||
"subtle 2.6.1",
|
||||
"subtle",
|
||||
"subtle-encoding",
|
||||
"tendermint",
|
||||
"tendermint-config",
|
||||
@@ -8437,7 +8306,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"subtle 2.6.1",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user