Add outer AEAD encryption + Ack message type for LP protocol

- Add OuterAeadKey derived from PSK via Blake3 KDF for packet encryption
- Add LpMessage::Ack (0x0008) for ClientHello acknowledgment
- Gateway sends Ack after processing ClientHello (packet-per-connection)
- Update codec with AEAD encrypt/decrypt using ChaCha20-Poly1305
- Header remains cleartext (AAD), payload encrypted after PSK derivation
- Add parse_lp_header_only() for routing before session lookup
- Update session to expose outer_aead_key() getter
- Various LP protocol improvements and test coverage

Closes: nym-f4v1, nym-n9dr
This commit is contained in:
durch
2025-11-27 12:39:48 +01:00
parent d9e9b73c1d
commit 53c1689011
19 changed files with 1537 additions and 634 deletions
Generated
+1
View File
@@ -6591,6 +6591,7 @@ dependencies = [
"bincode",
"bs58",
"bytes",
"chacha20poly1305",
"criterion",
"dashmap",
"libcrux-kem",
+1
View File
@@ -34,6 +34,7 @@ libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
libcrux-traits = { git = "https://github.com/cryspen/libcrux" }
tls_codec = { workspace = true }
num_enum = { workspace = true }
chacha20poly1305 = { workspace = true }
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
+331
View File
@@ -0,0 +1,331 @@
# LP Protocol Design
## Overview
The Lewes Protocol (LP) provides authenticated, encrypted sessions with replay protection. Key design principles:
1. **Unified packet structure** - Same format for all packet types
2. **Receiver index** - Client-proposed session identifier (replaces computed session_id)
3. **Opportunistic encryption** - Header authentication and payload encryption as soon as PSK is available
4. **WireGuard-inspired simplicity** - Minimal header, clear security model
## Packet Structure
```
┌─────────┬──────────┬────────────────┬─────────┬─────────────────────┬─────────┐
│ version │ reserved │ receiver_index │ counter │ payload │ trailer │
│ 1B │ 3B │ 4B │ 8B │ variable │ 16B │
└─────────┴──────────┴────────────────┴─────────┴─────────────────────┴─────────┘
16B header 16B
```
**Total overhead:** 32 bytes (16B header + 16B trailer)
### Field Descriptions
| Field | Size | Description |
|-------|------|-------------|
| version | 1 byte | Protocol version |
| reserved | 3 bytes | Reserved for future use |
| receiver_index | 4 bytes | Session identifier, proposed by client |
| counter | 8 bytes | Monotonic counter, used as AEAD nonce and for replay protection |
| payload | variable | Message type (2B) + content (plaintext or encrypted depending on state) |
| trailer | 16 bytes | Zeros (no PSK) or AEAD Poly1305 tag (with PSK) |
### Wire Format
Length-prefixed over TCP:
```
┌────────────────────┬─────────────────────────────────────────────────────┐
│ length (4B BE u32) │ LpPacket │
└────────────────────┴─────────────────────────────────────────────────────┘
```
## Message Types
| Type | Value | Description |
|------|-------|-------------|
| Busy | 0x0000 | Server congestion signal |
| Handshake | 0x0001 | Noise protocol messages |
| EncryptedData | 0x0002 | Encrypted application data |
| ClientHello | 0x0003 | Initial session setup |
| KKTRequest | 0x0004 | KEM key transfer request |
| KKTResponse | 0x0005 | KEM key transfer response |
| ForwardPacket | 0x0006 | Nested session forwarding |
| Collision | 0x0007 | Receiver index collision |
| SubsessionRequest | 0x0008 | Client requests new subsession |
| SubsessionKK1 | 0x0009 | KK handshake msg 1 (responder → initiator) |
| SubsessionKK2 | 0x000A | KK handshake msg 2 (initiator → responder) |
| SubsessionReady | 0x000B | Subsession established confirmation |
## Receiver Index
### Assignment
The client generates a random 4-byte receiver_index and includes it in ClientHello. The gateway uses this as the session lookup key. This replaces the previous approach of computing a deterministic session_id from both parties' keys.
### Collision Handling
With 4 bytes (2^32 values), collision probability is negligible:
| Active Sessions | Collision Probability |
|-----------------|----------------------|
| 10,000 | ~0.001% |
| 100,000 | ~0.1% |
If collision detected, gateway rejects ClientHello and client retries with new index.
## Opportunistic Encryption
### Principle
As soon as PSK is derived (after processing Noise msg 1 with PSQ), all subsequent packets use outer AEAD encryption:
- **Header**: Authenticated as associated data (AD)
- **Payload**: Encrypted (message type + content)
- **Trailer**: AEAD tag
### Timeline
| Packet | PSK Available | Header | Payload | Trailer |
|--------|---------------|--------|---------|---------|
| ClientHello | No | Clear | Clear | Zeros |
| KKTRequest | No | Clear | Clear | Zeros |
| KKTResponse | No | Clear | Clear | Zeros |
| Noise msg 1 | No | Clear | Clear | Zeros |
| | | **PSK derived** | | |
| Noise msg 2 | Yes | Authenticated | Encrypted | Tag |
| Noise msg 3 | Yes | Authenticated | Encrypted | Tag |
| Data | Yes | Authenticated | Encrypted | Tag |
### Encryption Scheme
- **AEAD**: ChaCha20-Poly1305
- **Key**: outer_key = KDF(PSK, "lp-outer-aead") - derived from PSK, not PSK itself
- **Nonce**: counter (8 bytes, zero-padded to 12 bytes)
- **AAD**: version ‖ reserved ‖ receiver_index ‖ counter (16 bytes)
Note: PSK is used as-is for Noise (which does internal key derivation). The outer_key derivation avoids key reuse between the two encryption layers.
### Before PSK
```
┌─────────┬──────────┬────────────────┬─────────┬─────────────────────┬─────────┐
│ version │ reserved │ receiver_index │ counter │ payload │ 00...00 │
│ │ │ │ │ (plaintext) │ │
└─────────┴──────────┴────────────────┴─────────┴─────────────────────┴─────────┘
│←──────────────────────────── cleartext ──────────────────────────────────────┤
```
### After PSK
```
┌─────────┬──────────┬────────────────┬─────────┬─────────────────────┬─────────┐
│ version │ reserved │ receiver_index │ counter │ payload │ tag │
│ │ │ │ │ (encrypted) │ │
└─────────┴──────────┴────────────────┴─────────┴─────────────────────┴─────────┘
│←───────── cleartext (authenticated via AAD) ─────────┤│← encrypted ─┤│─ auth ─┤
```
## Handshake Flow
```
Client Gateway
│ │
│ [hdr][ClientHello][zeros] │
│──────────────────────────────────────►│ store state[receiver_index]
│ │
│ [hdr][KKTRequest][zeros] │
│──────────────────────────────────────►│
│ │
│ [hdr][KKTResponse][zeros] │
│◄──────────────────────────────────────│
│ │
│ [hdr][Noise1+PSQ][zeros] │
│──────────────────────────────────────►│ derive PSK
│ │
│ [hdr][encrypted Noise2][tag] │ ← authenticated
│◄──────────────────────────────────────│
│ │
│ [hdr][encrypted Noise3][tag] │ ← authenticated
│──────────────────────────────────────►│
│ │
│ ════════ Session Established ═════════│
│ │
│ [hdr][encrypted Data][tag] │
│◄─────────────────────────────────────►│
```
## Data Packet Encryption
Data packets have two encryption layers:
```
Application Data
┌─────────────────────┐
│ Noise encrypt │ Inner layer (forward secrecy, ratcheting)
│ (session keys) │
└─────────────────────┘
┌─────────────────────┐
│ PSK AEAD │ Outer layer (header auth, payload encryption)
│ (pre-shared key) │
└─────────────────────┘
Wire: [header][encrypted payload][tag]
```
### What Outer AEAD Encrypts
The outer AEAD encrypts: message_type (2B) + message content
This hides the message type from observers after PSK is available.
## Subsessions and Rekeying
Subsessions enable **forward secrecy** through periodic rekeying and **channel multiplexing** for independent encrypted streams.
### Design Principles
| Aspect | Decision | Rationale |
|--------|----------|-----------|
| Key derivation | Noise KK handshake | Clean crypto, both parties already authenticated |
| Initiation channel | Tunneled through parent | Already authenticated, no proof-of-ownership needed |
| Hierarchy | Promotion model (chain) | Simpler than tree, natural for rekeying |
| Old session after promotion | Read-only until TTL | Drains in-flight packets, provides grace period |
### Noise KK Pattern
Subsessions use `Noise_KK_25519_ChaChaPoly_SHA256`:
- **KK** = Both parties already know each other's static keys
- **2 messages** to complete (vs 3 for XKpsk3)
- **No PSK needed** - already authenticated via parent session
### Promotion Model
When a subsession is created, it becomes the new "master" and the old session becomes read-only:
```
Session A (master) → Session B created → A demoted, B is master
A: read-only until TTL
```
This creates a chain (A → B → C) but maintains only one level of nesting conceptually. Each promotion replaces the previous master.
### Protocol Flow
```
Client Gateway
│ │
│═══════ Parent Session (A) ════════│ Transport mode
│ │
│──[SubsessionRequest{idx=B}]──────►│ Encrypted in parent
│ │ Gateway creates KK responder
│◄──[SubsessionKK1{idx=B, e}]───────│ KK handshake msg 1
│──[SubsessionKK2{idx=B, e,ee,se}]─►│ KK handshake msg 2
│◄──[SubsessionReady{idx=B}]────────│ Subsession established
│ │
│ Session A: read-only (receive) │
│═══════ Session B (new master) ════│ New Transport mode
```
### Session State Transitions
```
Parent Session (A):
Transport → ReadOnlyTransport (on subsession creation)
ReadOnlyTransport → (expires via TTL cleanup)
Subsession (B):
(created) → KKHandshaking → Transport (becomes new master)
```
### Read-Only Session Semantics
After demotion:
- **Can receive**: Decrypt and process incoming packets (drain in-flight)
- **Cannot send**: Encryption blocked, returns error
- **Cleaned up**: Via normal TTL expiration
### Message Formats
```rust
SubsessionRequestData {
new_receiver_index: u32, // Client-proposed index for subsession
}
SubsessionKK1Data {
new_receiver_index: u32,
kk_message: Vec<u8>, // Noise KK message 1
}
SubsessionKK2Data {
new_receiver_index: u32,
kk_message: Vec<u8>, // Noise KK message 2
}
SubsessionReadyData {
new_receiver_index: u32,
}
```
### Counter Independence
- Each session has independent counters
- Subsession starts at counter 0
- No counter coordination needed between parent and subsession
### Failure Handling
| Scenario | Action |
|----------|--------|
| KK handshake fails | Discard attempt, keep using parent |
| Receiver index collision | Retry with new receiver_index |
| Parent session not found | Return error, client reconnects |
### Security Benefits
1. **Forward secrecy**: Compromise of current keys doesn't expose past traffic
2. **Key rotation**: Periodic rekeying limits exposure window
3. **Channel isolation**: Independent streams can't cross-decrypt
## Security Properties
### Always Visible to Observer
- Version (1 byte)
- Reserved (3 bytes)
- Receiver index (4 bytes) - opaque, unlinkable to identity
- Counter (8 bytes) - reveals packet ordering
- Packet size
### Protected After PSK
- Header integrity (authenticated via AEAD AAD)
- Payload confidentiality (encrypted)
- Message type (hidden)
- Application data (double encrypted)
### Cryptographic Guarantees
| Property | Mechanism |
|----------|-----------|
| Confidentiality | ChaCha20 (outer) + Noise ChaCha20 (inner) |
| Integrity | Poly1305 (outer) + Noise Poly1305 (inner) |
| Replay protection | Counter validation (before decryption) |
| Forward secrecy | Noise session keys (inner) + subsession rekeying |
| Header authentication | AEAD associated data |
| Key rotation | Periodic subsession creation (Noise KK) |
## References
- WireGuard Protocol - Inspiration for receiver_index and packet simplicity
- Noise Protocol Framework - Inner encryption layer, KK pattern for subsessions
- RFC 8439 ChaCha20-Poly1305 - AEAD cipher
- Noise Explorer KK - https://noiseexplorer.com/patterns/KK/
+590 -116
View File
@@ -7,94 +7,238 @@ use crate::message::{
KKTResponseData, LpMessage, MessageType,
};
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
use bytes::BytesMut;
use bytes::{BufMut, BytesMut};
use chacha20poly1305::{
aead::{AeadInPlace, KeyInit},
ChaCha20Poly1305, Key, Nonce, Tag,
};
/// Outer AEAD key for LP packet encryption.
///
/// Derived from PSK using Blake3 KDF with domain separation.
/// Used for opportunistic encryption: before PSK packets are cleartext,
/// after PSK packets have encrypted payload and authenticated header.
///
/// # Security: Nonce Reuse Prevention
///
/// ChaCha20-Poly1305 requires unique nonces per key. The counter starts at 0
/// for each session, which is safe because:
///
/// 1. **PSK is always fresh**: Each handshake uses PSQ
/// with a client-generated random salt. This ensures a unique
/// PSK for every session, even between the same client-gateway pair.
///
/// 2. **Key derivation**: `outer_key = Blake3_KDF("lp-outer-aead", PSK)`.
/// Different PSK → different outer_key → nonce reuse impossible.
///
/// 3. **No PSK persistence**: PSK handles are not stored/reused across sessions.
/// Each connection performs fresh KKT+PSQ handshake.
///
#[derive(Clone)]
pub struct OuterAeadKey {
key: [u8; 32],
}
impl OuterAeadKey {
/// KDF context for outer AEAD key derivation (domain separation)
const KDF_CONTEXT: &'static str = "lp-outer-aead";
/// Derive outer AEAD key from PSK.
///
/// Uses Blake3 KDF with domain separation to avoid key reuse
/// between the outer AEAD layer and the inner Noise layer.
pub fn from_psk(psk: &[u8; 32]) -> Self {
let key = nym_crypto::kdf::derive_key_blake3(Self::KDF_CONTEXT, psk, &[]);
Self { key }
}
/// Get reference to the raw key bytes.
pub fn as_bytes(&self) -> &[u8; 32] {
&self.key
}
}
impl Drop for OuterAeadKey {
fn drop(&mut self) {
// Zeroize key material on drop
self.key.iter_mut().for_each(|b| *b = 0);
}
}
impl std::fmt::Debug for OuterAeadKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OuterAeadKey")
.field("key", &"[REDACTED]")
.finish()
}
}
/// Build 12-byte nonce from 8-byte counter (zero-padded).
///
/// Format: counter (8 bytes LE) || 0x00000000 (4 bytes)
fn build_nonce(counter: u64) -> [u8; 12] {
let mut nonce = [0u8; 12];
nonce[..8].copy_from_slice(&counter.to_le_bytes());
// bytes 8..12 remain zero (zero-padding)
nonce
}
/// Parse message from raw type and content bytes.
///
/// Used when decrypting outer-encrypted packets where the message type
/// was encrypted along with the content.
fn parse_message_from_type_and_content(
msg_type_raw: u16,
content: &[u8],
) -> Result<LpMessage, LpError> {
let message_type = MessageType::from_u16(msg_type_raw)
.ok_or_else(|| LpError::invalid_message_type(msg_type_raw))?;
match message_type {
MessageType::Busy => {
if !content.is_empty() {
return Err(LpError::InvalidPayloadSize {
expected: 0,
actual: content.len(),
});
}
Ok(LpMessage::Busy)
}
MessageType::Handshake => Ok(LpMessage::Handshake(HandshakeData(content.to_vec()))),
MessageType::EncryptedData => {
Ok(LpMessage::EncryptedData(EncryptedDataPayload(content.to_vec())))
}
MessageType::ClientHello => {
let data: ClientHelloData = bincode::deserialize(content)
.map_err(|e| LpError::DeserializationError(e.to_string()))?;
Ok(LpMessage::ClientHello(data))
}
MessageType::KKTRequest => Ok(LpMessage::KKTRequest(KKTRequestData(content.to_vec()))),
MessageType::KKTResponse => Ok(LpMessage::KKTResponse(KKTResponseData(content.to_vec()))),
MessageType::ForwardPacket => {
let data: ForwardPacketData = bincode::deserialize(content)
.map_err(|e| LpError::DeserializationError(e.to_string()))?;
Ok(LpMessage::ForwardPacket(data))
}
MessageType::Collision => {
if !content.is_empty() {
return Err(LpError::InvalidPayloadSize {
expected: 0,
actual: content.len(),
});
}
Ok(LpMessage::Collision)
}
MessageType::Ack => {
if !content.is_empty() {
return Err(LpError::InvalidPayloadSize {
expected: 0,
actual: content.len(),
});
}
Ok(LpMessage::Ack)
}
}
}
/// Parse only the LP header from raw packet bytes.
///
/// Used for routing before session lookup when the header is always cleartext.
/// This allows the caller to determine the receiver_idx and look up the appropriate
/// session to get the outer AEAD key before calling `parse_lp_packet()`.
///
/// # Arguments
/// * `src` - Raw packet bytes (at least LpHeader::SIZE bytes)
///
/// # Errors
/// * `LpError::InsufficientBufferSize` - Packet too small for header
pub fn parse_lp_header_only(src: &[u8]) -> Result<LpHeader, LpError> {
if src.len() < LpHeader::SIZE {
return Err(LpError::InsufficientBufferSize);
}
LpHeader::parse(&src[..LpHeader::SIZE])
}
/// Parses a complete Lewes Protocol packet from a byte slice (e.g., a UDP datagram payload).
///
/// Assumes the input `src` contains exactly one complete packet. It does not handle
/// stream fragmentation or provide replay protection checks (these belong at the session level).
pub fn parse_lp_packet(src: &[u8]) -> Result<LpPacket, LpError> {
///
/// # Arguments
/// * `src` - Raw packet bytes
/// * `outer_key` - None for cleartext parsing, Some for AEAD decryption
///
/// # Errors
/// * `LpError::AeadTagMismatch` - Tag verification failed (when outer_key provided)
/// * `LpError::InsufficientBufferSize` - Packet too small
pub fn parse_lp_packet(
src: &[u8],
outer_key: Option<&OuterAeadKey>,
) -> Result<LpPacket, LpError> {
// Minimum size check: LpHeader + Type + Trailer (for 0-payload message)
let min_size = LpHeader::SIZE + 2 + TRAILER_LEN;
if src.len() < min_size {
return Err(LpError::InsufficientBufferSize);
}
// Parse LpHeader
let header = LpHeader::parse(&src[..LpHeader::SIZE])?; // Uses the new LpHeader::parse
// Parse LpHeader (always cleartext for routing)
let header = LpHeader::parse(&src[..LpHeader::SIZE])?;
// Parse Message Type
let type_start = LpHeader::SIZE;
let type_end = type_start + 2;
let mut message_type_bytes = [0u8; 2];
message_type_bytes.copy_from_slice(&src[type_start..type_end]);
let message_type_raw = u16::from_le_bytes(message_type_bytes);
let message_type = MessageType::from_u16(message_type_raw)
.ok_or_else(|| LpError::invalid_message_type(message_type_raw))?;
// Extract trailer (potential AEAD tag)
let trailer_start = src.len() - TRAILER_LEN;
let mut trailer = [0u8; TRAILER_LEN];
trailer.copy_from_slice(&src[trailer_start..]);
// Calculate payload size based on total length
let total_size = src.len();
let message_size = total_size - min_size; // Size of the payload part
// Payload is everything between header and trailer
let payload_bytes = &src[LpHeader::SIZE..trailer_start];
// Extract payload based on message type
let message_start = type_end;
let message_end = message_start + message_size;
let payload_slice = &src[message_start..message_end]; // Bounds already checked by min_size and total_size calculation
let message = match message_type {
MessageType::Busy => {
if message_size != 0 {
return Err(LpError::InvalidPayloadSize {
expected: 0,
actual: message_size,
});
// Handle decryption if outer key provided
let (message_type_raw, message_content) = match outer_key {
None => {
// Cleartext mode - parse directly
if payload_bytes.len() < 2 {
return Err(LpError::InsufficientBufferSize);
}
LpMessage::Busy
let msg_type = u16::from_le_bytes([payload_bytes[0], payload_bytes[1]]);
(msg_type, &payload_bytes[2..])
}
MessageType::Handshake => {
// No size validation needed here for Handshake, it's variable
LpMessage::Handshake(HandshakeData(payload_slice.to_vec()))
}
MessageType::EncryptedData => {
// No size validation needed here for EncryptedData, it's variable
LpMessage::EncryptedData(EncryptedDataPayload(payload_slice.to_vec()))
}
MessageType::ClientHello => {
// ClientHello has structured data
// Deserialize ClientHelloData from payload
let data: ClientHelloData = bincode::deserialize(payload_slice)
.map_err(|e| LpError::DeserializationError(e.to_string()))?;
LpMessage::ClientHello(data)
}
MessageType::KKTRequest => {
// KKT request contains serialized KKTFrame bytes
LpMessage::KKTRequest(KKTRequestData(payload_slice.to_vec()))
}
MessageType::KKTResponse => {
// KKT response contains serialized KKTFrame bytes
LpMessage::KKTResponse(KKTResponseData(payload_slice.to_vec()))
}
MessageType::ForwardPacket => {
// ForwardPacket has structured data
let data: ForwardPacketData = bincode::deserialize(payload_slice)
.map_err(|e| LpError::DeserializationError(e.to_string()))?;
LpMessage::ForwardPacket(data)
Some(key) => {
// AEAD decryption mode
let nonce = build_nonce(header.counter);
let aad = &src[..LpHeader::SIZE]; // Header as AAD
// Copy payload for in-place decryption
let mut decrypted = payload_bytes.to_vec();
// Convert trailer to Tag
let tag = Tag::from_slice(&trailer);
// Decrypt and verify
let cipher = ChaCha20Poly1305::new(Key::from_slice(key.as_bytes()));
cipher
.decrypt_in_place_detached(Nonce::from_slice(&nonce), aad, &mut decrypted, tag)
.map_err(|_| LpError::AeadTagMismatch)?;
// Extract message type from decrypted payload
if decrypted.len() < 2 {
return Err(LpError::InsufficientBufferSize);
}
let msg_type = u16::from_le_bytes([decrypted[0], decrypted[1]]);
// Return decrypted content (owned, so we handle it differently)
return parse_message_from_type_and_content(msg_type, &decrypted[2..]).map(|message| {
LpPacket {
header,
message,
trailer,
}
});
}
};
// Extract trailer
let trailer_start = message_end;
let trailer_end = trailer_start + TRAILER_LEN;
// Check if trailer_end exceeds src length (shouldn't happen if min_size check passed and calculation is correct, but good for safety)
if trailer_end > total_size {
// This indicates an internal logic error or buffer manipulation issue
return Err(LpError::InsufficientBufferSize); // Or a more specific internal error
}
let trailer_slice = &src[trailer_start..trailer_end];
let mut trailer = [0u8; TRAILER_LEN];
trailer.copy_from_slice(trailer_slice);
// Cleartext path: parse message from payload
let message = parse_message_from_type_and_content(message_type_raw, message_content)?;
// Create and return the packet
Ok(LpPacket {
header,
message,
@@ -103,11 +247,66 @@ pub fn parse_lp_packet(src: &[u8]) -> Result<LpPacket, LpError> {
}
/// Serializes an LpPacket into the provided BytesMut buffer.
pub fn serialize_lp_packet(item: &LpPacket, dst: &mut BytesMut) -> Result<(), LpError> {
// Reserve approximate size - consider making this more accurate if needed
dst.reserve(LpHeader::SIZE + 2 + item.message.len() + TRAILER_LEN);
item.encode(dst); // Use the existing encode method on LpPacket
Ok(())
///
/// # Arguments
/// * `item` - Packet to serialize
/// * `dst` - Output buffer
/// * `outer_key` - None for cleartext (uses packet's trailer), Some for AEAD encryption
///
/// When `outer_key` is provided:
/// - Header is written in cleartext (used as AAD)
/// - Message type + content is encrypted
/// - Trailer is set to the AEAD tag
pub fn serialize_lp_packet(
item: &LpPacket,
dst: &mut BytesMut,
outer_key: Option<&OuterAeadKey>,
) -> Result<(), LpError> {
match outer_key {
None => {
// Cleartext mode - use existing encode method
dst.reserve(LpHeader::SIZE + 2 + item.message.len() + TRAILER_LEN);
item.encode(dst);
Ok(())
}
Some(key) => {
// AEAD encryption mode
dst.reserve(LpHeader::SIZE + 2 + item.message.len() + TRAILER_LEN);
// 1. Encode header (AAD - not encrypted)
let header_start = dst.len();
item.header.encode(dst);
let header_end = dst.len();
// 2. Build plaintext: message_type (2B) + content
let mut plaintext = BytesMut::new();
plaintext.put_slice(&(item.message.typ() as u16).to_le_bytes());
item.message.encode_content(&mut plaintext);
// 3. Copy plaintext to dst for in-place encryption
let payload_start = dst.len();
dst.put_slice(&plaintext);
// 4. Build nonce and get AAD
let nonce = build_nonce(item.header.counter);
let aad = &dst[header_start..header_end].to_vec(); // Copy AAD since we mutate dst
// 5. Encrypt payload in-place
let cipher = ChaCha20Poly1305::new(Key::from_slice(key.as_bytes()));
let tag = cipher
.encrypt_in_place_detached(
Nonce::from_slice(&nonce),
aad,
&mut dst[payload_start..],
)
.map_err(|_| LpError::Internal("AEAD encryption failed".to_string()))?;
// 6. Append tag as trailer
dst.put_slice(&tag);
Ok(())
}
}
}
// Add a new error variant for invalid message types (Moved from previous impl LpError block)
@@ -120,14 +319,17 @@ impl LpError {
#[cfg(test)]
mod tests {
// Import standalone functions
use super::{parse_lp_packet, serialize_lp_packet};
use super::{parse_lp_packet, serialize_lp_packet, OuterAeadKey};
// Keep necessary imports
use crate::LpError;
use crate::message::{EncryptedDataPayload, HandshakeData, LpMessage, MessageType};
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
use bytes::BytesMut;
// === Updated Encode/Decode Tests ===
// Header length: version(1) + reserved(3) + receiver_index(4) + counter(8) = 16 bytes
const HEADER_LEN: usize = 16;
// === Cleartext Encode/Decode Tests ===
#[test]
fn test_serialize_parse_busy() {
@@ -138,22 +340,22 @@ mod tests {
header: LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 42,
receiver_idx: 42,
counter: 123,
},
message: LpMessage::Busy,
trailer: [0; TRAILER_LEN],
};
// Serialize the packet
serialize_lp_packet(&packet, &mut dst).unwrap();
// Serialize the packet (cleartext)
serialize_lp_packet(&packet, &mut dst, None).unwrap();
// Parse the packet
let decoded = parse_lp_packet(&dst).unwrap();
// Parse the packet (cleartext)
let decoded = parse_lp_packet(&dst, None).unwrap();
// Verify the packet fields
assert_eq!(decoded.header.protocol_version, 1);
assert_eq!(decoded.header.session_id, 42);
assert_eq!(decoded.header.receiver_idx, 42);
assert_eq!(decoded.header.counter, 123);
assert!(matches!(decoded.message, LpMessage::Busy));
assert_eq!(decoded.trailer, [0; TRAILER_LEN]);
@@ -169,22 +371,22 @@ mod tests {
header: LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 42,
receiver_idx: 42,
counter: 123,
},
message: LpMessage::Handshake(HandshakeData(payload.clone())),
trailer: [0; TRAILER_LEN],
};
// Serialize the packet
serialize_lp_packet(&packet, &mut dst).unwrap();
// Serialize the packet (cleartext)
serialize_lp_packet(&packet, &mut dst, None).unwrap();
// Parse the packet
let decoded = parse_lp_packet(&dst).unwrap();
// Parse the packet (cleartext)
let decoded = parse_lp_packet(&dst, None).unwrap();
// Verify the packet fields
assert_eq!(decoded.header.protocol_version, 1);
assert_eq!(decoded.header.session_id, 42);
assert_eq!(decoded.header.receiver_idx, 42);
assert_eq!(decoded.header.counter, 123);
// Verify message type and data
@@ -207,22 +409,22 @@ mod tests {
header: LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 42,
receiver_idx: 42,
counter: 123,
},
message: LpMessage::EncryptedData(EncryptedDataPayload(payload.clone())),
trailer: [0; TRAILER_LEN],
};
// Serialize the packet
serialize_lp_packet(&packet, &mut dst).unwrap();
// Serialize the packet (cleartext)
serialize_lp_packet(&packet, &mut dst, None).unwrap();
// Parse the packet
let decoded = parse_lp_packet(&dst).unwrap();
// Parse the packet (cleartext)
let decoded = parse_lp_packet(&dst, None).unwrap();
// Verify the packet fields
assert_eq!(decoded.header.protocol_version, 1);
assert_eq!(decoded.header.session_id, 42);
assert_eq!(decoded.header.receiver_idx, 42);
assert_eq!(decoded.header.counter, 123);
// Verify message type and data
@@ -235,7 +437,7 @@ mod tests {
assert_eq!(decoded.trailer, [0; TRAILER_LEN]);
}
// === Updated Incomplete Data Tests ===
// === Incomplete Data Tests ===
#[test]
fn test_parse_incomplete_header() {
@@ -244,7 +446,7 @@ mod tests {
buf.extend_from_slice(&[1, 0, 0, 0]); // Only 4 bytes, not enough for LpHeader::SIZE
// Attempt to parse - expect error
let result = parse_lp_packet(&buf);
let result = parse_lp_packet(&buf, None);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
@@ -262,7 +464,7 @@ mod tests {
buf.extend_from_slice(&[0]); // Only 1 byte of message type (need 2)
// Buffer length = 16 + 1 = 17. Min size = 16 + 2 + 16 = 34.
let result = parse_lp_packet(&buf);
let result = parse_lp_packet(&buf, None);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
@@ -298,7 +500,7 @@ mod tests {
buf_too_short.extend_from_slice(&123u64.to_le_bytes()); // Counter
buf_too_short.extend_from_slice(&MessageType::Handshake.to_u16().to_le_bytes()); // Handshake type
// No payload, no trailer. Length = 16+2=18. Min size = 34.
let result_too_short = parse_lp_packet(&buf_too_short);
let result_too_short = parse_lp_packet(&buf_too_short, None);
assert!(result_too_short.is_err());
assert!(matches!(
result_too_short.unwrap_err(),
@@ -335,7 +537,7 @@ mod tests {
buf_too_short.extend_from_slice(&MessageType::Busy.to_u16().to_le_bytes()); // Type
buf_too_short.extend_from_slice(&[0; TRAILER_LEN - 1]); // Missing last byte of trailer
// Length = 16 + 2 + 15 = 33. Min Size = 34.
let result_too_short = parse_lp_packet(&buf_too_short);
let result_too_short = parse_lp_packet(&buf_too_short, None);
assert!(
result_too_short.is_err(),
"Expected error for buffer size 33, min 34"
@@ -360,7 +562,7 @@ mod tests {
buf.extend_from_slice(&[0; TRAILER_LEN]); // Trailer
// Attempt to parse
let result = parse_lp_packet(&buf);
let result = parse_lp_packet(&buf, None);
assert!(result.is_err());
match result {
Err(LpError::InvalidMessageType(255)) => {} // Expected error
@@ -382,7 +584,7 @@ mod tests {
// Total size = 16 + 2 + 1 + 16 = 35. Min size = 34.
// Calculated payload size = 35 - 34 = 1.
let result = parse_lp_packet(&buf);
let result = parse_lp_packet(&buf, None);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
@@ -410,6 +612,7 @@ mod tests {
let client_ed25519_key = [43u8; 32];
let salt = [99u8; 32];
let hello_data = ClientHelloData {
receiver_index: 12345,
client_lp_public_key: client_key,
client_ed25519_public_key: client_ed25519_key,
salt,
@@ -420,7 +623,7 @@ mod tests {
header: LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 42,
receiver_idx: 42,
counter: 123,
},
message: LpMessage::ClientHello(hello_data.clone()),
@@ -428,14 +631,14 @@ mod tests {
};
// Serialize the packet
serialize_lp_packet(&packet, &mut dst).unwrap();
serialize_lp_packet(&packet, &mut dst, None).unwrap();
// Parse the packet
let decoded = parse_lp_packet(&dst).unwrap();
let decoded = parse_lp_packet(&dst, None).unwrap();
// Verify the packet fields
assert_eq!(decoded.header.protocol_version, 1);
assert_eq!(decoded.header.session_id, 42);
assert_eq!(decoded.header.receiver_idx, 42);
assert_eq!(decoded.header.counter, 123);
// Verify message type and data
@@ -465,7 +668,7 @@ mod tests {
header: LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 100,
receiver_idx: 100,
counter: 200,
},
message: LpMessage::ClientHello(hello_data.clone()),
@@ -473,10 +676,10 @@ mod tests {
};
// Serialize the packet
serialize_lp_packet(&packet, &mut dst).unwrap();
serialize_lp_packet(&packet, &mut dst, None).unwrap();
// Parse the packet
let decoded = parse_lp_packet(&dst).unwrap();
let decoded = parse_lp_packet(&dst, None).unwrap();
// Verify message type and data
match decoded.message {
@@ -511,7 +714,7 @@ mod tests {
buf.extend_from_slice(&[0; TRAILER_LEN]); // Trailer
// Attempt to parse
let result = parse_lp_packet(&buf);
let result = parse_lp_packet(&buf, None);
assert!(result.is_err());
match result {
Err(LpError::DeserializationError(_)) => {} // Expected error
@@ -534,7 +737,7 @@ mod tests {
buf.extend_from_slice(&[0; TRAILER_LEN]); // Trailer
// Attempt to parse
let result = parse_lp_packet(&buf);
let result = parse_lp_packet(&buf, None);
assert!(result.is_err());
match result {
Err(LpError::DeserializationError(_)) => {} // Expected error
@@ -551,6 +754,7 @@ mod tests {
let mut dst = BytesMut::new();
let hello_data = ClientHelloData {
receiver_index: version as u32,
client_lp_public_key: [version; 32],
client_ed25519_public_key: [version.wrapping_add(2); 32],
salt: [version.wrapping_add(1); 32],
@@ -560,15 +764,15 @@ mod tests {
header: LpHeader {
protocol_version: 1,
reserved: 0,
session_id: version as u32,
receiver_idx: version as u32,
counter: version as u64,
},
message: LpMessage::ClientHello(hello_data.clone()),
trailer: [version; TRAILER_LEN],
};
serialize_lp_packet(&packet, &mut dst).unwrap();
let decoded = parse_lp_packet(&dst).unwrap();
serialize_lp_packet(&packet, &mut dst, None).unwrap();
let decoded = parse_lp_packet(&dst, None).unwrap();
match decoded.message {
LpMessage::ClientHello(decoded_data) => {
@@ -593,7 +797,7 @@ mod tests {
header: LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 999,
receiver_idx: 999,
counter: 555,
},
message: LpMessage::ForwardPacket(forward_data),
@@ -601,13 +805,13 @@ mod tests {
};
// Serialize
serialize_lp_packet(&packet, &mut dst).unwrap();
serialize_lp_packet(&packet, &mut dst, None).unwrap();
// Parse back
let decoded = parse_lp_packet(&dst).unwrap();
let decoded = parse_lp_packet(&dst, None).unwrap();
// Verify LP protocol handling works correctly
assert_eq!(decoded.header.session_id, 999);
assert_eq!(decoded.header.receiver_idx, 999);
assert!(matches!(decoded.message.typ(), MessageType::ForwardPacket));
if let LpMessage::ForwardPacket(data) = decoded.message {
@@ -618,4 +822,274 @@ mod tests {
panic!("Expected ForwardPacket message");
}
}
// === Outer AEAD Tests ===
#[test]
fn test_aead_roundtrip_with_key() {
// Test that encrypt/decrypt roundtrip works with an AEAD key
let psk = [42u8; 32];
let outer_key = OuterAeadKey::from_psk(&psk);
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: 0,
receiver_idx: 12345,
counter: 999,
},
message: LpMessage::Busy,
trailer: [0; TRAILER_LEN],
};
let mut encrypted = BytesMut::new();
serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap();
// Parse back with the same key
let decoded = parse_lp_packet(&encrypted, Some(&outer_key)).unwrap();
assert_eq!(decoded.header.protocol_version, 1);
assert_eq!(decoded.header.receiver_idx, 12345);
assert_eq!(decoded.header.counter, 999);
assert!(matches!(decoded.message, LpMessage::Busy));
}
#[test]
fn test_aead_ciphertext_differs_from_plaintext() {
// Verify that encrypted payload differs from plaintext
let psk = [42u8; 32];
let outer_key = OuterAeadKey::from_psk(&psk);
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: 0,
receiver_idx: 12345,
counter: 999,
},
message: LpMessage::EncryptedData(crate::message::EncryptedDataPayload(vec![
0xAA, 0xBB, 0xCC, 0xDD,
])),
trailer: [0; TRAILER_LEN],
};
let mut cleartext = BytesMut::new();
serialize_lp_packet(&packet, &mut cleartext, None).unwrap();
let mut encrypted = BytesMut::new();
serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap();
// Header should be the same (it's authenticated but not encrypted)
assert_eq!(&cleartext[..HEADER_LEN], &encrypted[..HEADER_LEN]);
// Payload should differ (it's encrypted)
let payload_start = HEADER_LEN;
let payload_end_cleartext = cleartext.len() - TRAILER_LEN;
let payload_end_encrypted = encrypted.len() - TRAILER_LEN;
assert_ne!(
&cleartext[payload_start..payload_end_cleartext],
&encrypted[payload_start..payload_end_encrypted],
"Encrypted payload should differ from plaintext"
);
// Trailer should differ (zeros vs AEAD tag)
assert_ne!(
&cleartext[payload_end_cleartext..],
&encrypted[payload_end_encrypted..],
"Encrypted trailer should be a tag, not zeros"
);
}
#[test]
fn test_aead_tampered_tag_fails() {
// Verify that tampering with the tag causes decryption failure
let psk = [42u8; 32];
let outer_key = OuterAeadKey::from_psk(&psk);
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: 0,
receiver_idx: 12345,
counter: 999,
},
message: LpMessage::Busy,
trailer: [0; TRAILER_LEN],
};
let mut encrypted = BytesMut::new();
serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap();
// Tamper with the tag (last byte)
let last_idx = encrypted.len() - 1;
encrypted[last_idx] ^= 0xFF;
// Parsing should fail with AeadTagMismatch
let result = parse_lp_packet(&encrypted, Some(&outer_key));
assert!(matches!(result, Err(LpError::AeadTagMismatch)));
}
#[test]
fn test_aead_tampered_header_fails() {
// Verify that tampering with the header (AAD) causes decryption failure
let psk = [42u8; 32];
let outer_key = OuterAeadKey::from_psk(&psk);
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: 0,
receiver_idx: 12345,
counter: 999,
},
message: LpMessage::Busy,
trailer: [0; TRAILER_LEN],
};
let mut encrypted = BytesMut::new();
serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap();
// Tamper with the header (flip a bit in receiver_idx)
encrypted[4] ^= 0x01;
// Parsing should fail with AeadTagMismatch
let result = parse_lp_packet(&encrypted, Some(&outer_key));
assert!(matches!(result, Err(LpError::AeadTagMismatch)));
}
#[test]
fn test_aead_different_counters_produce_different_ciphertext() {
// Verify that different counters (nonces) produce different ciphertexts
let psk = [42u8; 32];
let outer_key = OuterAeadKey::from_psk(&psk);
let packet1 = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: 0,
receiver_idx: 12345,
counter: 1,
},
message: LpMessage::Busy,
trailer: [0; TRAILER_LEN],
};
let packet2 = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: 0,
receiver_idx: 12345,
counter: 2, // Different counter
},
message: LpMessage::Busy,
trailer: [0; TRAILER_LEN],
};
let mut encrypted1 = BytesMut::new();
serialize_lp_packet(&packet1, &mut encrypted1, Some(&outer_key)).unwrap();
let mut encrypted2 = BytesMut::new();
serialize_lp_packet(&packet2, &mut encrypted2, Some(&outer_key)).unwrap();
// The encrypted payloads should differ even though the message is the same
// (because nonce is different)
let payload_start = HEADER_LEN;
assert_ne!(
&encrypted1[payload_start..],
&encrypted2[payload_start..],
"Different counters should produce different ciphertexts"
);
}
#[test]
fn test_aead_wrong_key_fails() {
// Verify that decryption with wrong key fails
let psk1 = [42u8; 32];
let psk2 = [43u8; 32]; // Different PSK
let outer_key1 = OuterAeadKey::from_psk(&psk1);
let outer_key2 = OuterAeadKey::from_psk(&psk2);
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: 0,
receiver_idx: 12345,
counter: 999,
},
message: LpMessage::Busy,
trailer: [0; TRAILER_LEN],
};
let mut encrypted = BytesMut::new();
serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key1)).unwrap();
// Parsing with wrong key should fail
let result = parse_lp_packet(&encrypted, Some(&outer_key2));
assert!(matches!(result, Err(LpError::AeadTagMismatch)));
}
#[test]
fn test_aead_encrypted_data_message_roundtrip() {
// Test AEAD with EncryptedData message type (larger payload)
let psk = [42u8; 32];
let outer_key = OuterAeadKey::from_psk(&psk);
let payload_data = vec![0xDE; 100];
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: 0,
receiver_idx: 54321,
counter: 12345678,
},
message: LpMessage::EncryptedData(crate::message::EncryptedDataPayload(
payload_data.clone(),
)),
trailer: [0; TRAILER_LEN],
};
let mut encrypted = BytesMut::new();
serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap();
let decoded = parse_lp_packet(&encrypted, Some(&outer_key)).unwrap();
match decoded.message {
LpMessage::EncryptedData(data) => {
assert_eq!(data.0, payload_data);
}
_ => panic!("Expected EncryptedData message"),
}
}
#[test]
fn test_aead_handshake_message_roundtrip() {
// Test AEAD with Handshake message type
let psk = [42u8; 32];
let outer_key = OuterAeadKey::from_psk(&psk);
let handshake_data = vec![0x01, 0x02, 0x03, 0x04, 0x05];
let packet = LpPacket {
header: LpHeader {
protocol_version: 1,
reserved: 0,
receiver_idx: 99999,
counter: 2,
},
message: LpMessage::Handshake(HandshakeData(handshake_data.clone())),
trailer: [0; TRAILER_LEN],
};
let mut encrypted = BytesMut::new();
serialize_lp_packet(&packet, &mut encrypted, Some(&outer_key)).unwrap();
let decoded = parse_lp_packet(&encrypted, Some(&outer_key)).unwrap();
match decoded.message {
LpMessage::Handshake(data) => {
assert_eq!(data.0, handshake_data);
}
_ => panic!("Expected Handshake message"),
}
}
}
+4
View File
@@ -78,4 +78,8 @@ pub enum LpError {
/// Ed25519 to X25519 conversion error.
#[error("Ed25519 key conversion error: {0}")]
Ed25519RecoveryError(#[from] Ed25519RecoveryError),
/// Outer AEAD authentication tag verification failed.
#[error("AEAD authentication tag verification failed")]
AeadTagMismatch,
}
+35 -85
View File
@@ -14,12 +14,9 @@ pub mod session;
mod session_integration;
pub mod session_manager;
use std::hash::{DefaultHasher, Hasher as _};
pub use error::LpError;
use keypair::PublicKey;
pub use message::{ClientHelloData, LpMessage};
pub use packet::{LpPacket, BOOTSTRAP_SESSION_ID};
pub use packet::{LpPacket, BOOTSTRAP_RECEIVER_IDX};
pub use replay::{ReceivingKeyCounterValidator, ReplayError};
pub use session::{LpSession, generate_fresh_salt};
pub use session_manager::SessionManager;
@@ -33,13 +30,15 @@ pub const NOISE_PSK_INDEX: u8 = 3;
#[cfg(test)]
pub fn sessions_for_tests() -> (LpSession, LpSession) {
use crate::{keypair::Keypair, make_lp_id};
use crate::keypair::Keypair;
use nym_crypto::asymmetric::ed25519;
// X25519 keypairs for Noise protocol
let keypair_1 = Keypair::default();
let keypair_2 = Keypair::default();
let id = make_lp_id(keypair_1.public_key(), keypair_2.public_key());
// Use a fixed receiver_index for deterministic tests
let receiver_index: u32 = 12345;
// Ed25519 keypairs for PSQ authentication (placeholders for testing)
let ed25519_keypair_1 = ed25519::KeyPair::from_secret([1u8; 32], 0);
@@ -51,7 +50,7 @@ pub fn sessions_for_tests() -> (LpSession, LpSession) {
// PSQ will always derive the PSK during handshake using X25519 as DHKEM
let initiator_session = LpSession::new(
id,
receiver_index,
true,
(
ed25519_keypair_1.private_key(),
@@ -65,7 +64,7 @@ pub fn sessions_for_tests() -> (LpSession, LpSession) {
.expect("Test session creation failed");
let responder_session = LpSession::new(
id,
receiver_index,
false,
(
ed25519_keypair_2.private_key(),
@@ -81,47 +80,12 @@ pub fn sessions_for_tests() -> (LpSession, LpSession) {
(initiator_session, responder_session)
}
/// Generates a deterministic u32 session ID for the Lewes Protocol
/// based on two public keys. The order of the keys does not matter.
///
/// Uses a different internal delimiter than `make_conv_id` to avoid
/// potential collisions if the same key pairs were used in both contexts.
fn make_id(key1_bytes: &[u8], key2_bytes: &[u8], sep: u8) -> u32 {
let mut hasher = DefaultHasher::new();
// Ensure consistent order for hashing to make the ID order-independent.
// This guarantees make_lp_id(a, b) == make_lp_id(b, a).
if key1_bytes < key2_bytes {
hasher.write(key1_bytes);
// Use a delimiter specific to Lewes Protocol ID generation
// (0xCC chosen arbitrarily, could be any value different from 0xFF)
hasher.write_u8(sep);
hasher.write(key2_bytes);
} else {
hasher.write(key2_bytes);
hasher.write_u8(sep);
hasher.write(key1_bytes);
}
// Truncate the u64 hash result to u32
(hasher.finish() & 0xFFFF_FFFF) as u32
}
pub fn make_lp_id(key1_bytes: &PublicKey, key2_bytes: &PublicKey) -> u32 {
make_id(key1_bytes.as_bytes(), key2_bytes.as_bytes(), 0xCC)
}
pub fn make_conv_id(src: &[u8], dst: &[u8]) -> u32 {
make_id(src, dst, 0xFF)
}
#[cfg(test)]
mod tests {
use crate::keypair::PublicKey;
use crate::message::LpMessage;
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
use crate::session_manager::SessionManager;
use crate::{LpError, make_lp_id, sessions_for_tests};
use crate::{LpError, sessions_for_tests};
use bytes::BytesMut;
// Import the new standalone functions
@@ -137,7 +101,7 @@ mod tests {
header: LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 42, // Matches session's sending_index assumption for this test
receiver_idx: 42, // Matches session's sending_index assumption for this test
counter: 0,
},
message: LpMessage::Busy,
@@ -146,10 +110,10 @@ mod tests {
// Serialize packet
let mut buf1 = BytesMut::new();
serialize_lp_packet(&packet1, &mut buf1).unwrap();
serialize_lp_packet(&packet1, &mut buf1, None).unwrap();
// Parse packet
let parsed_packet1 = parse_lp_packet(&buf1).unwrap();
let parsed_packet1 = parse_lp_packet(&buf1, None).unwrap();
// Perform replay check (should pass)
session
@@ -166,7 +130,7 @@ mod tests {
header: LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 42,
receiver_idx: 42,
counter: 0, // Same counter as before (replay)
},
message: LpMessage::Busy,
@@ -175,10 +139,10 @@ mod tests {
// Serialize packet
let mut buf2 = BytesMut::new();
serialize_lp_packet(&packet2, &mut buf2).unwrap();
serialize_lp_packet(&packet2, &mut buf2, None).unwrap();
// Parse packet
let parsed_packet2 = parse_lp_packet(&buf2).unwrap();
let parsed_packet2 = parse_lp_packet(&buf2, None).unwrap();
// Perform replay check (should fail)
let replay_result = session.receiving_counter_quick_check(parsed_packet2.header.counter);
@@ -196,7 +160,7 @@ mod tests {
header: LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 42,
receiver_idx: 42,
counter: 1, // Incremented counter
},
message: LpMessage::Busy,
@@ -205,10 +169,10 @@ mod tests {
// Serialize packet
let mut buf3 = BytesMut::new();
serialize_lp_packet(&packet3, &mut buf3).unwrap();
serialize_lp_packet(&packet3, &mut buf3, None).unwrap();
// Parse packet
let parsed_packet3 = parse_lp_packet(&buf3).unwrap();
let parsed_packet3 = parse_lp_packet(&buf3, None).unwrap();
// Perform replay check (should pass)
session
@@ -238,24 +202,8 @@ mod tests {
let ed25519_keypair_local = ed25519::KeyPair::from_secret([8u8; 32], 0);
let ed25519_keypair_remote = ed25519::KeyPair::from_secret([9u8; 32], 1);
// Derive X25519 keys from Ed25519 (same as state machine does internally)
let x25519_pub_local = ed25519_keypair_local
.public_key()
.to_x25519()
.expect("Failed to derive X25519 from Ed25519");
let x25519_pub_remote = ed25519_keypair_remote
.public_key()
.to_x25519()
.expect("Failed to derive X25519 from Ed25519");
// Convert to LP keypair types
let lp_pub_local = PublicKey::from_bytes(x25519_pub_local.as_bytes())
.expect("Failed to create PublicKey from bytes");
let lp_pub_remote = PublicKey::from_bytes(x25519_pub_remote.as_bytes())
.expect("Failed to create PublicKey from bytes");
// Calculate lp_id (matches state machine's internal calculation)
let lp_id = make_lp_id(&lp_pub_local, &lp_pub_remote);
// Use fixed receiver_index for deterministic test
let receiver_index: u32 = 54321;
// Test salt
let salt = [46u8; 32];
@@ -263,6 +211,7 @@ mod tests {
// Create a session via manager
let _ = local_manager
.create_session_state_machine(
receiver_index,
(
ed25519_keypair_local.private_key(),
ed25519_keypair_local.public_key(),
@@ -275,6 +224,7 @@ mod tests {
let _ = remote_manager
.create_session_state_machine(
receiver_index,
(
ed25519_keypair_remote.private_key(),
ed25519_keypair_remote.public_key(),
@@ -289,7 +239,7 @@ mod tests {
header: LpHeader {
protocol_version: 1,
reserved: 0,
session_id: lp_id,
receiver_idx: receiver_index,
counter: 0,
},
message: LpMessage::Busy,
@@ -298,10 +248,10 @@ mod tests {
// Serialize
let mut buf1 = BytesMut::new();
serialize_lp_packet(&packet1, &mut buf1).unwrap();
serialize_lp_packet(&packet1, &mut buf1, None).unwrap();
// Parse
let parsed_packet1 = parse_lp_packet(&buf1).unwrap();
let parsed_packet1 = parse_lp_packet(&buf1, None).unwrap();
// Process via SessionManager method (which should handle checks + marking)
// NOTE: We might need a method on SessionManager/LpSession like `process_incoming_packet`
@@ -310,11 +260,11 @@ mod tests {
// Perform replay check
local_manager
.receiving_counter_quick_check(lp_id, parsed_packet1.header.counter)
.receiving_counter_quick_check(receiver_index, parsed_packet1.header.counter)
.expect("Packet 1 check failed");
// Mark received
local_manager
.receiving_counter_mark(lp_id, parsed_packet1.header.counter)
.receiving_counter_mark(receiver_index, parsed_packet1.header.counter)
.expect("Packet 1 mark failed");
// === Packet 2 (Counter 1 - Should succeed on same session) ===
@@ -322,7 +272,7 @@ mod tests {
header: LpHeader {
protocol_version: 1,
reserved: 0,
session_id: lp_id,
receiver_idx: receiver_index,
counter: 1,
},
message: LpMessage::Busy,
@@ -331,18 +281,18 @@ mod tests {
// Serialize
let mut buf2 = BytesMut::new();
serialize_lp_packet(&packet2, &mut buf2).unwrap();
serialize_lp_packet(&packet2, &mut buf2, None).unwrap();
// Parse
let parsed_packet2 = parse_lp_packet(&buf2).unwrap();
let parsed_packet2 = parse_lp_packet(&buf2, None).unwrap();
// Perform replay check
local_manager
.receiving_counter_quick_check(lp_id, parsed_packet2.header.counter)
.receiving_counter_quick_check(receiver_index, parsed_packet2.header.counter)
.expect("Packet 2 check failed");
// Mark received
local_manager
.receiving_counter_mark(lp_id, parsed_packet2.header.counter)
.receiving_counter_mark(receiver_index, parsed_packet2.header.counter)
.expect("Packet 2 mark failed");
// === Packet 3 (Counter 0 - Replay, should fail check) ===
@@ -350,7 +300,7 @@ mod tests {
header: LpHeader {
protocol_version: 1,
reserved: 0,
session_id: lp_id,
receiver_idx: receiver_index,
counter: 0, // Replay of first packet
},
message: LpMessage::Busy,
@@ -359,14 +309,14 @@ mod tests {
// Serialize
let mut buf3 = BytesMut::new();
serialize_lp_packet(&packet3, &mut buf3).unwrap();
serialize_lp_packet(&packet3, &mut buf3, None).unwrap();
// Parse
let parsed_packet3 = parse_lp_packet(&buf3).unwrap();
let parsed_packet3 = parse_lp_packet(&buf3, None).unwrap();
// Perform replay check (should fail)
let replay_result =
local_manager.receiving_counter_quick_check(lp_id, parsed_packet3.header.counter);
local_manager.receiving_counter_quick_check(receiver_index, parsed_packet3.header.counter);
assert!(replay_result.is_err());
match replay_result.unwrap_err() {
LpError::Replay(e) => {
+27 -2
View File
@@ -9,6 +9,9 @@ use serde::{Deserialize, Serialize};
/// Data structure for the ClientHello message
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientHelloData {
/// Client-proposed receiver index for session identification (4 bytes)
/// Auto-generated randomly by the client
pub receiver_index: u32,
/// Client's LP x25519 public key (32 bytes) - derived from Ed25519 key
pub client_lp_public_key: [u8; 32],
/// Client's Ed25519 public key (32 bytes) - for PSQ authentication
@@ -46,6 +49,7 @@ impl ClientHelloData {
rand::thread_rng().fill_bytes(&mut salt[8..]);
Self {
receiver_index: rand::random(), // Auto-generate random receiver index
client_lp_public_key,
client_ed25519_public_key,
salt,
@@ -73,6 +77,10 @@ pub enum MessageType {
KKTRequest = 0x0004,
KKTResponse = 0x0005,
ForwardPacket = 0x0006,
/// Receiver index collision - client should retry with new index
Collision = 0x0007,
/// Acknowledgment - gateway confirms receipt of message
Ack = 0x0008,
}
impl MessageType {
@@ -122,6 +130,10 @@ pub enum LpMessage {
KKTRequest(KKTRequestData),
KKTResponse(KKTResponseData),
ForwardPacket(ForwardPacketData),
/// Receiver index collision - client should retry with new receiver_index
Collision,
/// Acknowledgment - gateway confirms receipt of message
Ack,
}
impl Display for LpMessage {
@@ -134,6 +146,8 @@ impl Display for LpMessage {
LpMessage::KKTRequest(_) => write!(f, "KKTRequest"),
LpMessage::KKTResponse(_) => write!(f, "KKTResponse"),
LpMessage::ForwardPacket(_) => write!(f, "ForwardPacket"),
LpMessage::Collision => write!(f, "Collision"),
LpMessage::Ack => write!(f, "Ack"),
}
}
}
@@ -148,6 +162,8 @@ impl LpMessage {
LpMessage::KKTRequest(payload) => payload.0.as_slice(),
LpMessage::KKTResponse(payload) => payload.0.as_slice(),
LpMessage::ForwardPacket(_) => &[], // Structured data, serialized in encode_content
LpMessage::Collision => &[],
LpMessage::Ack => &[],
}
}
@@ -160,6 +176,8 @@ impl LpMessage {
LpMessage::KKTRequest(payload) => payload.0.is_empty(),
LpMessage::KKTResponse(payload) => payload.0.is_empty(),
LpMessage::ForwardPacket(_) => false, // Always has data
LpMessage::Collision => true,
LpMessage::Ack => true,
}
}
@@ -168,12 +186,15 @@ impl LpMessage {
LpMessage::Busy => 0,
LpMessage::Handshake(payload) => payload.0.len(),
LpMessage::EncryptedData(payload) => payload.0.len(),
LpMessage::ClientHello(_) => 97, // 32 bytes x25519 key + 32 bytes ed25519 key + 32 bytes salt + 1 byte bincode overhead
// 4 bytes receiver_index + 32 bytes x25519 key + 32 bytes ed25519 key + 32 bytes salt + bincode overhead
LpMessage::ClientHello(_) => 101,
LpMessage::KKTRequest(payload) => payload.0.len(),
LpMessage::KKTResponse(payload) => payload.0.len(),
LpMessage::ForwardPacket(data) => {
32 + data.target_lp_address.len() + data.inner_packet_bytes.len() + 10
}
LpMessage::Collision => 0,
LpMessage::Ack => 0,
}
}
@@ -186,6 +207,8 @@ impl LpMessage {
LpMessage::KKTRequest(_) => MessageType::KKTRequest,
LpMessage::KKTResponse(_) => MessageType::KKTResponse,
LpMessage::ForwardPacket(_) => MessageType::ForwardPacket,
LpMessage::Collision => MessageType::Collision,
LpMessage::Ack => MessageType::Ack,
}
}
@@ -215,6 +238,8 @@ impl LpMessage {
bincode::serialize(data).expect("Failed to serialize ForwardPacketData");
dst.put_slice(&serialized);
}
LpMessage::Collision => { /* No content */ }
LpMessage::Ack => { /* No content */ }
}
}
}
@@ -232,7 +257,7 @@ mod tests {
let resp_header = LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 0,
receiver_idx: 0,
counter: 0,
};
+12 -12
View File
@@ -124,18 +124,18 @@ impl LpPacket {
/// Session ID used for ClientHello bootstrap packets before session is established.
///
/// When a client first connects, it sends a ClientHello packet with session_id=0
/// When a client first connects, it sends a ClientHello packet with receiver_idx=0
/// because neither side can compute the deterministic session ID yet (requires
/// both parties' X25519 keys). After ClientHello is processed, both sides derive
/// the same session ID from their keys, and all subsequent packets use that ID.
pub const BOOTSTRAP_SESSION_ID: u32 = 0;
pub const BOOTSTRAP_RECEIVER_IDX: u32 = 0;
// VERSION [1B] || RESERVED [3B] || SENDER_INDEX [4B] || COUNTER [8B]
#[derive(Debug, Clone)]
pub struct LpHeader {
pub protocol_version: u8,
pub reserved: u16,
pub session_id: u32,
pub receiver_idx: u32,
pub counter: u64,
}
@@ -144,11 +144,11 @@ impl LpHeader {
}
impl LpHeader {
pub fn new(session_id: u32, counter: u64) -> Self {
pub fn new(receiver_idx: u32, counter: u64) -> Self {
Self {
protocol_version: 1,
reserved: 0,
session_id,
receiver_idx,
counter,
}
}
@@ -161,7 +161,7 @@ impl LpHeader {
dst.put_slice(&[0, 0, 0]);
// sender index
dst.put_slice(&self.session_id.to_le_bytes());
dst.put_slice(&self.receiver_idx.to_le_bytes());
// counter
dst.put_slice(&self.counter.to_le_bytes());
@@ -175,9 +175,9 @@ impl LpHeader {
let protocol_version = src[0];
// Skip reserved bytes [1..4]
let mut session_id_bytes = [0u8; 4];
session_id_bytes.copy_from_slice(&src[4..8]);
let session_id = u32::from_le_bytes(session_id_bytes);
let mut receiver_idx_bytes = [0u8; 4];
receiver_idx_bytes.copy_from_slice(&src[4..8]);
let receiver_idx = u32::from_le_bytes(receiver_idx_bytes);
let mut counter_bytes = [0u8; 8];
counter_bytes.copy_from_slice(&src[8..16]);
@@ -186,7 +186,7 @@ impl LpHeader {
Ok(LpHeader {
protocol_version,
reserved: 0,
session_id,
receiver_idx,
counter,
})
}
@@ -197,8 +197,8 @@ impl LpHeader {
}
/// Get the sender index from the header
pub fn session_id(&self) -> u32 {
self.session_id
pub fn receiver_idx(&self) -> u32 {
self.receiver_idx
}
}
+60 -30
View File
@@ -6,6 +6,7 @@
//! This module implements session management functionality, including replay protection
//! and Noise protocol state handling.
use crate::codec::OuterAeadKey;
use crate::keypair::{PrivateKey, PublicKey};
use crate::message::{EncryptedDataPayload, HandshakeData};
use crate::noise_protocol::{NoiseError, NoiseProtocol, ReadResult};
@@ -165,6 +166,10 @@ pub struct LpSession {
/// Salt for PSK derivation
salt: [u8; 32],
/// Outer AEAD key for packet encryption (derived from PSK after PSQ handshake).
/// None before PSK is available, Some after PSK injection.
outer_aead_key: Mutex<Option<OuterAeadKey>>,
}
/// Generates a fresh salt for PSK derivation.
@@ -217,6 +222,17 @@ impl LpSession {
self.local_x25519_private.public_key()
}
/// Returns the outer AEAD key for packet encryption/decryption.
///
/// Returns `None` before PSK is derived (during initial handshake),
/// `Some(&OuterAeadKey)` after PSK injection via PSQ.
///
/// Callers should use `None` for packet encryption/decryption during
/// the handshake phase, and use the returned key for transport phase.
pub fn outer_aead_key(&self) -> Option<OuterAeadKey> {
self.outer_aead_key.lock().clone()
}
/// Creates a new session and initializes the Noise protocol state.
///
/// PSQ always runs during the handshake to derive the real PSK from X25519 DHKEM.
@@ -301,6 +317,7 @@ impl LpSession {
local_x25519_private: local_x25519_key.clone(),
remote_x25519_public: remote_x25519_key.clone(),
salt: *salt,
outer_aead_key: Mutex::new(None),
})
}
@@ -638,6 +655,12 @@ impl LpSession {
// Mark PSK as injected for safety checks in transport mode
self.psk_injected.store(true, Ordering::Release);
// Derive and store outer AEAD key from PSK
{
let mut outer_key = self.outer_aead_key.lock();
*outer_key = Some(OuterAeadKey::from_psk(&psk));
}
// Get the Noise handshake message
let noise_msg = match noise_state.get_bytes_to_send() {
Some(Ok(msg)) => msg,
@@ -801,6 +824,12 @@ impl LpSession {
// Mark PSK as injected for safety checks in transport mode
self.psk_injected.store(true, Ordering::Release);
// Derive and store outer AEAD key from PSK
{
let mut outer_key = self.outer_aead_key.lock();
*outer_key = Some(OuterAeadKey::from_psk(&psk));
}
// Update PSQ state to Completed
*psq_state = PSQState::Completed { psk };
@@ -946,15 +975,13 @@ mod tests {
// Helper function to create a session with real keys for handshake tests
fn create_handshake_test_session(
receiver_index: u32,
is_initiator: bool,
local_keys: &crate::keypair::Keypair,
remote_pub_key: &crate::keypair::PublicKey,
) -> LpSession {
use nym_crypto::asymmetric::ed25519;
// Compute the shared lp_id from both keypairs (order-independent)
let lp_id = crate::make_lp_id(local_keys.public_key(), remote_pub_key);
// Create Ed25519 keypairs that correspond to initiator/responder roles
// Initiator uses [1u8], Responder uses [2u8]
let (local_ed25519_seed, remote_ed25519_seed) = if is_initiator {
@@ -970,7 +997,7 @@ mod tests {
// PSQ will derive the PSK during handshake using X25519 as DHKEM
let session = LpSession::new(
lp_id,
receiver_index,
is_initiator,
(local_ed25519.private_key(), local_ed25519.public_key()),
local_keys.private_key(),
@@ -1080,10 +1107,12 @@ mod tests {
fn test_prepare_handshake_message_initial_state() {
let initiator_keys = generate_keypair();
let responder_keys = generate_keypair();
let receiver_index = 12345u32;
let initiator_session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(receiver_index, true, &initiator_keys, responder_keys.public_key());
let responder_session = create_handshake_test_session(
receiver_index,
false,
&responder_keys,
initiator_keys.public_key(), // Responder also needs initiator's key for XK
@@ -1106,11 +1135,12 @@ mod tests {
fn test_process_handshake_message_first_step() {
let initiator_keys = generate_keypair();
let responder_keys = generate_keypair();
let receiver_index = 12345u32;
let initiator_session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(receiver_index, true, &initiator_keys, responder_keys.public_key());
let responder_session =
create_handshake_test_session(false, &responder_keys, initiator_keys.public_key());
create_handshake_test_session(receiver_index, false, &responder_keys, initiator_keys.public_key());
// 1. Initiator prepares the first message (-> e)
let initiator_msg_result = initiator_session.prepare_handshake_message();
@@ -1145,9 +1175,9 @@ mod tests {
let responder_keys = generate_keypair();
let initiator_session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(12345u32, true, &initiator_keys, responder_keys.public_key());
let responder_session =
create_handshake_test_session(false, &responder_keys, initiator_keys.public_key());
create_handshake_test_session(12345u32, false, &responder_keys, initiator_keys.public_key());
let mut responder_to_initiator_msg = None;
let mut rounds = 0;
@@ -1232,9 +1262,9 @@ mod tests {
let responder_keys = generate_keypair();
let initiator_session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(12345u32, true, &initiator_keys, responder_keys.public_key());
let responder_session =
create_handshake_test_session(false, &responder_keys, initiator_keys.public_key());
create_handshake_test_session(12345u32, false, &responder_keys, initiator_keys.public_key());
// Drive handshake to completion (simplified loop from previous test)
let mut i_msg = initiator_session
@@ -1293,7 +1323,7 @@ mod tests {
let responder_keys = generate_keypair();
let initiator_session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(12345u32, true, &initiator_keys, responder_keys.public_key());
assert!(!initiator_session.is_handshake_complete());
@@ -1365,9 +1395,9 @@ mod tests {
let responder_keys = generate_keypair();
let initiator_session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(12345u32, true, &initiator_keys, responder_keys.public_key());
let responder_session =
create_handshake_test_session(false, &responder_keys, initiator_keys.public_key());
create_handshake_test_session(12345u32, false, &responder_keys, initiator_keys.public_key());
// Drive the handshake
let mut i_msg = initiator_session
@@ -1459,9 +1489,9 @@ mod tests {
// Create sessions - they start with dummy PSK [0u8; 32]
let initiator_session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(12345u32, true, &initiator_keys, responder_keys.public_key());
let responder_session =
create_handshake_test_session(false, &responder_keys, initiator_keys.public_key());
create_handshake_test_session(12345u32, false, &responder_keys, initiator_keys.public_key());
// Prepare first message (initiator runs PSQ and injects PSK)
let i_msg = initiator_session
@@ -1524,9 +1554,9 @@ mod tests {
let responder_keys = generate_keypair();
let initiator_session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(12345u32, true, &initiator_keys, responder_keys.public_key());
let responder_session =
create_handshake_test_session(false, &responder_keys, initiator_keys.public_key());
create_handshake_test_session(12345u32, false, &responder_keys, initiator_keys.public_key());
// Verify initial state
assert!(!initiator_session.is_handshake_complete());
@@ -1603,9 +1633,9 @@ mod tests {
// Create sessions with explicit Ed25519 keys
let initiator_session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(12345u32, true, &initiator_keys, responder_keys.public_key());
let responder_session =
create_handshake_test_session(false, &responder_keys, initiator_keys.public_key());
create_handshake_test_session(12345u32, false, &responder_keys, initiator_keys.public_key());
// Verify sessions store Ed25519 keys
// (Internal verification - keys are used in PSQ calls)
@@ -1648,7 +1678,7 @@ mod tests {
let initiator_keys = generate_keypair();
let responder_session =
create_handshake_test_session(false, &responder_keys, initiator_keys.public_key());
create_handshake_test_session(12345u32, false, &responder_keys, initiator_keys.public_key());
// Create a handshake message with corrupted PSQ payload
let corrupted_psq_data = vec![0xFF; 128]; // Random garbage
@@ -1677,11 +1707,11 @@ mod tests {
let initiator_ed25519 = ed25519::KeyPair::from_secret([1u8; 32], 0);
let wrong_ed25519 = ed25519::KeyPair::from_secret([99u8; 32], 99); // Different key!
let lp_id = crate::make_lp_id(initiator_keys.public_key(), responder_keys.public_key());
let receiver_index: u32 = 55555;
let salt = [0u8; 32];
let initiator_session = LpSession::new(
lp_id,
receiver_index,
true,
(
initiator_ed25519.private_key(),
@@ -1699,7 +1729,7 @@ mod tests {
let responder_ed25519 = ed25519::KeyPair::from_secret([2u8; 32], 1);
let responder_session = LpSession::new(
lp_id,
receiver_index,
false,
(
responder_ed25519.private_key(),
@@ -1748,11 +1778,11 @@ mod tests {
let wrong_ed25519_keypair = ed25519::KeyPair::from_secret([99u8; 32], 99);
let wrong_ed25519_public = wrong_ed25519_keypair.public_key();
let lp_id = crate::make_lp_id(initiator_keys.public_key(), responder_keys.public_key());
let receiver_index: u32 = 66666;
let salt = [0u8; 32];
let initiator_session = LpSession::new(
lp_id,
receiver_index,
true,
(
initiator_ed25519.private_key(),
@@ -1770,7 +1800,7 @@ mod tests {
let responder_ed25519 = ed25519::KeyPair::from_secret([2u8; 32], 1);
let responder_session = LpSession::new(
lp_id,
receiver_index,
false,
(
responder_ed25519.private_key(),
@@ -1813,7 +1843,7 @@ mod tests {
let initiator_keys = generate_keypair();
let responder_session =
create_handshake_test_session(false, &responder_keys, initiator_keys.public_key());
create_handshake_test_session(12345u32, false, &responder_keys, initiator_keys.public_key());
// Capture initial PSQ state (should be ResponderWaiting)
// (We can't directly access psq_state, but we can verify behavior)
@@ -1831,7 +1861,7 @@ mod tests {
// Session should still be functional - can process valid messages
// Create a proper initiator to send valid message
let initiator_session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(12345u32, true, &initiator_keys, responder_keys.public_key());
let valid_msg = initiator_session
.prepare_handshake_message()
@@ -1858,7 +1888,7 @@ mod tests {
// Create session but don't complete handshake (no PSK injection will occur)
let session =
create_handshake_test_session(true, &initiator_keys, responder_keys.public_key());
create_handshake_test_session(12345u32, true, &initiator_keys, responder_keys.public_key());
// Verify session was created successfully
assert!(!session.is_handshake_complete());
+116 -108
View File
@@ -2,7 +2,6 @@
mod tests {
use crate::codec::{parse_lp_packet, serialize_lp_packet};
use crate::keypair::PublicKey;
use crate::make_lp_id;
use crate::{
LpError,
message::LpMessage,
@@ -15,7 +14,7 @@ mod tests {
// Function to create a test packet - similar to how it's done in codec.rs tests
fn create_test_packet(
protocol_version: u8,
session_id: u32,
receiver_idx: u32,
counter: u64,
message: LpMessage,
) -> LpPacket {
@@ -23,7 +22,7 @@ mod tests {
let header = LpHeader {
protocol_version,
reserved: 0u16, // reserved
session_id,
receiver_idx,
counter,
};
@@ -54,7 +53,7 @@ mod tests {
let ed25519_keypair_a = ed25519::KeyPair::from_secret([1u8; 32], 0);
let ed25519_keypair_b = ed25519::KeyPair::from_secret([2u8; 32], 1);
// Derive X25519 keys from Ed25519 (same as state machine does internally)
// Derive X25519 keys from Ed25519 (needed for KKT init test)
let x25519_pub_a = ed25519_keypair_a
.public_key()
.to_x25519()
@@ -70,8 +69,8 @@ mod tests {
let lp_pub_b = PublicKey::from_bytes(x25519_pub_b.as_bytes())
.expect("Failed to create PublicKey from bytes");
// Calculate lp_id (matches state machine's internal calculation)
let lp_id = make_lp_id(&lp_pub_a, &lp_pub_b);
// Use fixed receiver_index for deterministic test
let receiver_index: u32 = 100001;
// Test salt
let salt = [42u8; 32];
@@ -79,6 +78,7 @@ mod tests {
// 4. Create sessions using the pre-built Noise states
let peer_a_sm = session_manager_1
.create_session_state_machine(
receiver_index,
(
ed25519_keypair_a.private_key(),
ed25519_keypair_a.public_key(),
@@ -91,6 +91,7 @@ mod tests {
let peer_b_sm = session_manager_2
.create_session_state_machine(
receiver_index,
(
ed25519_keypair_b.private_key(),
ed25519_keypair_b.public_key(),
@@ -145,13 +146,13 @@ mod tests {
);
// A prepares packet
let counter = session_manager_1.next_counter(lp_id).unwrap();
let message_a_to_b = create_test_packet(1, lp_id, counter, payload);
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).expect("A serialize failed");
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).expect("B parse failed");
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
@@ -197,12 +198,12 @@ mod tests {
// B prepares packet
let counter = session_manager_2.next_counter(peer_b_sm).unwrap();
let message_b_to_a = create_test_packet(1, lp_id, counter, payload);
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).expect("B serialize failed");
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).expect("A parse failed");
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
@@ -282,13 +283,13 @@ mod tests {
// A prepares packet
let counter_a = session_manager_1.next_counter(peer_a_sm).unwrap();
let message_a_to_b = create_test_packet(1, lp_id, counter_a, ciphertext_a_to_b);
let message_a_to_b = create_test_packet(1, receiver_index, counter_a, ciphertext_a_to_b);
let mut encoded_data_a_to_b = BytesMut::new();
serialize_lp_packet(&message_a_to_b, &mut encoded_data_a_to_b)
serialize_lp_packet(&message_a_to_b, &mut encoded_data_a_to_b, None)
.expect("A serialize data failed");
// B parses packet and checks replay
let decoded_packet_b = parse_lp_packet(&encoded_data_a_to_b).expect("B parse data failed");
let decoded_packet_b = parse_lp_packet(&encoded_data_a_to_b, None).expect("B parse data failed");
assert_eq!(decoded_packet_b.header.counter, counter_a);
// Check replay before decrypting
@@ -316,13 +317,13 @@ mod tests {
.encrypt_data(peer_b_sm, plaintext_b_to_a)
.expect("B encrypt failed");
let counter_b = session_manager_2.next_counter(peer_b_sm).unwrap();
let message_b_to_a = create_test_packet(1, lp_id, counter_b, ciphertext_b_to_a);
let message_b_to_a = create_test_packet(1, receiver_index, counter_b, ciphertext_b_to_a);
let mut encoded_data_b_to_a = BytesMut::new();
serialize_lp_packet(&message_b_to_a, &mut encoded_data_b_to_a)
serialize_lp_packet(&message_b_to_a, &mut encoded_data_b_to_a, None)
.expect("B serialize data failed");
// A parses packet and checks replay
let decoded_packet_a = parse_lp_packet(&encoded_data_b_to_a).expect("A parse data failed");
let decoded_packet_a = parse_lp_packet(&encoded_data_b_to_a, None).expect("A parse data failed");
assert_eq!(decoded_packet_a.header.counter, counter_b);
// Check replay before decrypting
@@ -352,18 +353,18 @@ mod tests {
// Need to re-encode because decode consumes the buffer
let message_b_to_a_replay = create_test_packet(
1,
lp_id,
receiver_index,
counter_b,
LpMessage::EncryptedData(crate::message::EncryptedDataPayload(
plaintext_b_to_a.to_vec(),
)), // Using plaintext here, but content doesn't matter for replay check
);
let mut encoded_data_b_to_a_replay = BytesMut::new();
serialize_lp_packet(&message_b_to_a_replay, &mut encoded_data_b_to_a_replay)
serialize_lp_packet(&message_b_to_a_replay, &mut encoded_data_b_to_a_replay, None)
.expect("B serialize replay failed");
let parsed_replay_packet =
parse_lp_packet(&encoded_data_b_to_a_replay).expect("A parse replay failed");
parse_lp_packet(&encoded_data_b_to_a_replay, None).expect("A parse replay failed");
let replay_result = session_manager_1
.receiving_counter_quick_check(peer_a_sm, parsed_replay_packet.header.counter);
assert!(replay_result.is_err(), "Data replay should be prevented");
@@ -386,18 +387,18 @@ mod tests {
let message_a_to_b_skip = create_test_packet(
1, // protocol version
lp_id,
receiver_index,
counter_a_skip, // Send N+1 first
ciphertext_skip,
);
// Encode the skip message
let mut encoded_skip = BytesMut::new();
serialize_lp_packet(&message_a_to_b_skip, &mut encoded_skip)
serialize_lp_packet(&message_a_to_b_skip, &mut encoded_skip, None)
.expect("Failed to serialize skip message");
// B parses skip message and checks replay
let decoded_packet_skip = parse_lp_packet(&encoded_skip).expect("B parse skip failed");
let decoded_packet_skip = parse_lp_packet(&encoded_skip, None).expect("B parse skip failed");
session_manager_2
.receiving_counter_quick_check(peer_b_sm, decoded_packet_skip.header.counter)
.expect("B replay check skip failed");
@@ -428,14 +429,14 @@ mod tests {
let message_a_to_b_delayed = create_test_packet(
1, // protocol version
lp_id,
receiver_index,
counter_a_next, // counter N (delayed packet)
ciphertext_delayed,
);
// Encode the delayed message
let mut encoded_delayed = BytesMut::new();
serialize_lp_packet(&message_a_to_b_delayed, &mut encoded_delayed)
serialize_lp_packet(&message_a_to_b_delayed, &mut encoded_delayed, None)
.expect("Failed to serialize delayed message");
// Make a copy for replay test later
@@ -443,7 +444,7 @@ mod tests {
// B parses delayed message and checks replay
let decoded_packet_delayed =
parse_lp_packet(&encoded_delayed).expect("B parse delayed failed");
parse_lp_packet(&encoded_delayed, None).expect("B parse delayed failed");
session_manager_2
.receiving_counter_quick_check(peer_b_sm, decoded_packet_delayed.header.counter)
.expect("B replay check delayed failed");
@@ -469,7 +470,7 @@ mod tests {
// 11. 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).expect("Parse delayed replay failed");
parse_lp_packet(&encoded_delayed_copy, None).expect("Parse delayed replay failed");
let result = session_manager_2
.receiving_counter_quick_check(peer_b_sm, parsed_delayed_replay.header.counter);
assert!(result.is_err(), "Replay attack should be prevented");
@@ -479,15 +480,15 @@ mod tests {
);
// 12. Session removal
assert!(session_manager_1.remove_state_machine(lp_id));
assert!(session_manager_1.remove_state_machine(receiver_index));
assert_eq!(session_manager_1.session_count(), 0);
// Verify the session is gone
let session = session_manager_1.state_machine_exists(lp_id);
let session = session_manager_1.state_machine_exists(receiver_index);
assert!(!session, "Session should be removed");
// But the other session still exists
let session = session_manager_2.state_machine_exists(lp_id);
let session = session_manager_2.state_machine_exists(receiver_index);
assert!(session, "Session still exists in the other manager");
}
@@ -518,14 +519,15 @@ mod tests {
let lp_pub_b = PublicKey::from_bytes(x25519_pub_b.as_bytes())
.expect("Failed to create PublicKey from bytes");
// Calculate lp_id (matches state machine's internal calculation)
let lp_id = make_lp_id(&lp_pub_a, &lp_pub_b);
// Use fixed receiver_index for test
let receiver_index: u32 = 100002;
// Test salt
let salt = [43u8; 32];
let peer_a_sm = session_manager_1
.create_session_state_machine(
receiver_index,
(
ed25519_keypair_a.private_key(),
ed25519_keypair_a.public_key(),
@@ -537,6 +539,7 @@ mod tests {
.unwrap();
let peer_b_sm = session_manager_2
.create_session_state_machine(
receiver_index,
(
ed25519_keypair_b.private_key(),
ed25519_keypair_b.public_key(),
@@ -612,12 +615,12 @@ mod tests {
let current_counter_a = counter_a;
counter_a += 1;
let message_a = create_test_packet(1, lp_id, current_counter_a, ciphertext_a);
let message_a = create_test_packet(1, receiver_index, current_counter_a, ciphertext_a);
let mut encoded_a = BytesMut::new();
serialize_lp_packet(&message_a, &mut encoded_a).expect("A serialize failed");
serialize_lp_packet(&message_a, &mut encoded_a, None).expect("A serialize failed");
// B parses and checks replay
let decoded_packet_b = parse_lp_packet(&encoded_a).expect("B parse failed");
let decoded_packet_b = parse_lp_packet(&encoded_a, None).expect("B parse failed");
session_manager_2
.receiving_counter_quick_check(peer_b_sm, decoded_packet_b.header.counter)
.expect("B replay check failed (A->B)");
@@ -638,12 +641,12 @@ mod tests {
let current_counter_b = counter_b;
counter_b += 1;
let message_b = create_test_packet(1, lp_id, current_counter_b, ciphertext_b);
let message_b = create_test_packet(1, receiver_index, current_counter_b, ciphertext_b);
let mut encoded_b = BytesMut::new();
serialize_lp_packet(&message_b, &mut encoded_b).expect("B serialize failed");
serialize_lp_packet(&message_b, &mut encoded_b, None).expect("B serialize failed");
// A parses and checks replay
let decoded_packet_a = parse_lp_packet(&encoded_b).expect("A parse failed");
let decoded_packet_a = parse_lp_packet(&encoded_b, None).expect("A parse failed");
session_manager_1
.receiving_counter_quick_check(peer_a_sm, decoded_packet_a.header.counter)
.expect("A replay check failed (B->A)");
@@ -716,12 +719,12 @@ mod tests {
.to_x25519()
.expect("Failed to derive X25519 from Ed25519");
// Convert to LP keypair type
let lp_pub = PublicKey::from_bytes(x25519_pub.as_bytes())
// Convert to LP keypair type (still needed for init_kkt_for_test below if used)
let _lp_pub = PublicKey::from_bytes(x25519_pub.as_bytes())
.expect("Failed to create PublicKey from bytes");
// Calculate lp_id (self-connection: both sides use same key)
let lp_id = make_lp_id(&lp_pub, &lp_pub);
// Use fixed receiver_index for test
let receiver_index: u32 = 100003;
// Test salt
let salt = [44u8; 32];
@@ -729,6 +732,7 @@ mod tests {
// 2. Create a session (using real noise state)
let _session = session_manager
.create_session_state_machine(
receiver_index,
(ed25519_keypair.private_key(), ed25519_keypair.public_key()),
ed25519_keypair.public_key(),
true,
@@ -748,8 +752,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,
(ed25519_keypair.private_key(), ed25519_keypair.public_key()),
ed25519_keypair.public_key(),
true,
@@ -758,7 +764,7 @@ mod tests {
.expect("Failed to create temp session");
assert!(
session_manager.remove_state_machine(lp_id),
session_manager.remove_state_machine(receiver_index_temp),
"Should remove the session"
);
@@ -770,7 +776,7 @@ mod tests {
// Add header
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
buf.extend_from_slice(&lp_id.to_le_bytes()); // Sender index
buf.extend_from_slice(&receiver_index.to_le_bytes()); // Sender index
buf.extend_from_slice(&0u64.to_le_bytes()); // Counter
// Add invalid message type
@@ -783,7 +789,7 @@ mod tests {
buf.extend_from_slice(&[0u8; TRAILER_LEN]);
// Try to parse the invalid message type
let result = parse_lp_packet(&buf);
let result = parse_lp_packet(&buf, None);
assert!(result.is_err(), "Decoding invalid message type should fail");
// Add assertion for the specific error type
@@ -796,7 +802,7 @@ mod tests {
let partial_packet = &buf[0..10]; // Too short to be a valid packet
let partial_bytes = BytesMut::from(partial_packet);
let result = parse_lp_packet(&partial_bytes);
let result = parse_lp_packet(&partial_bytes, None);
assert!(result.is_err(), "Parsing partial packet should fail");
assert!(matches!(
result.unwrap_err(),
@@ -844,14 +850,14 @@ mod tests {
.to_x25519()
.expect("Failed to derive X25519 from Ed25519");
// Convert to LP keypair types
// Convert to LP keypair types (needed for init_kkt_for_test if used)
let lp_pub_a = PublicKey::from_bytes(x25519_pub_a.as_bytes())
.expect("Failed to create PublicKey from bytes");
let lp_pub_b = PublicKey::from_bytes(x25519_pub_b.as_bytes())
.expect("Failed to create PublicKey from bytes");
// Calculate lp_id (matches state machine's internal calculation)
let lp_id = make_lp_id(&lp_pub_a, &lp_pub_b);
// Use fixed receiver_index for test
let receiver_index: u32 = 100005;
// Test salt
let salt = [45u8; 32];
@@ -860,6 +866,7 @@ mod tests {
assert!(
session_manager_1
.create_session_state_machine(
receiver_index,
(
ed25519_keypair_a.private_key(),
ed25519_keypair_a.public_key()
@@ -873,6 +880,7 @@ mod tests {
assert!(
session_manager_2
.create_session_state_machine(
receiver_index,
(
ed25519_keypair_b.private_key(),
ed25519_keypair_b.public_key()
@@ -886,16 +894,16 @@ mod tests {
assert_eq!(session_manager_1.session_count(), 1);
assert_eq!(session_manager_2.session_count(), 1);
assert!(session_manager_1.state_machine_exists(lp_id));
assert!(session_manager_2.state_machine_exists(lp_id));
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(lp_id).unwrap(),
session_manager_1.get_state(receiver_index).unwrap(),
LpStateBare::ReadyToHandshake
);
assert_eq!(
session_manager_2.get_state(lp_id).unwrap(),
session_manager_2.get_state(receiver_index).unwrap(),
LpStateBare::ReadyToHandshake
);
@@ -910,7 +918,7 @@ mod tests {
// --- Round 1: Initiator Starts ---
println!(" Round {}: Initiator starts handshake", rounds);
let action_a1 = session_manager_1
.process_input(lp_id, LpInput::StartHandshake)
.process_input(receiver_index, LpInput::StartHandshake)
.expect("Initiator StartHandshake should produce an action")
.expect("Initiator StartHandshake failed");
@@ -922,7 +930,7 @@ mod tests {
}
// After StartHandshake, initiator should be in KKTExchange state (not Handshaking yet)
assert_eq!(
session_manager_1.get_state(lp_id).unwrap(),
session_manager_1.get_state(receiver_index).unwrap(),
LpStateBare::KKTExchange,
"Initiator state wrong after StartHandshake (should be KKTExchange)"
);
@@ -932,7 +940,7 @@ mod tests {
" Round {}: Responder explicitly enters KKTExchange state",
rounds
);
let action_b_start = session_manager_2.process_input(lp_id, LpInput::StartHandshake);
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(),
@@ -941,7 +949,7 @@ mod tests {
);
// Verify responder transitions to KKTExchange state (not Handshaking yet)
assert_eq!(
session_manager_2.get_state(lp_id).unwrap(),
session_manager_2.get_state(receiver_index).unwrap(),
LpStateBare::KKTExchange, // Responder also enters KKTExchange state
"Responder state should be KKTExchange after its StartHandshake"
);
@@ -959,12 +967,12 @@ mod tests {
// Simulate network: serialize -> parse (optional but good practice)
let mut buf_a = BytesMut::new();
serialize_lp_packet(&packet_to_process, &mut buf_a).unwrap();
let parsed_packet_a = parse_lp_packet(&buf_a).unwrap();
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(lp_id, LpInput::ReceivePacket(parsed_packet_a))
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_a))
.expect("Responder ReceivePacket should produce an action")
.expect("Responder ReceivePacket failed");
@@ -976,7 +984,7 @@ mod tests {
}
// Responder transitions to Handshaking after KKT completes
assert_eq!(
session_manager_2.get_state(lp_id).unwrap(),
session_manager_2.get_state(receiver_index).unwrap(),
LpStateBare::Handshaking,
"Responder state should be Handshaking after KKT exchange"
);
@@ -993,12 +1001,12 @@ mod tests {
// Simulate network
let mut buf_b = BytesMut::new();
serialize_lp_packet(&packet_to_process, &mut buf_b).unwrap();
let parsed_packet_b = parse_lp_packet(&buf_b).unwrap();
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(lp_id, LpInput::ReceivePacket(parsed_packet_b))
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_b))
.expect("Initiator ReceivePacket should produce an action")
.expect("Initiator ReceivePacket failed");
@@ -1010,7 +1018,7 @@ mod tests {
packet_a_to_b = Some(packet);
// Initiator transitions to Handshaking after KKT completes
assert_eq!(
session_manager_1.get_state(lp_id).unwrap(),
session_manager_1.get_state(receiver_index).unwrap(),
LpStateBare::Handshaking,
"Initiator state should be Handshaking after receiving KKT response"
);
@@ -1022,10 +1030,10 @@ mod tests {
// 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(lp_id) {
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, lp_id, 0, msg);
let packet = create_test_packet(1, receiver_index, 0, msg);
packet_a_to_b = Some(packet);
println!(" Prepared first Noise message after KKTComplete");
} else {
@@ -1052,12 +1060,12 @@ mod tests {
// Simulate network
let mut buf_a2 = BytesMut::new();
serialize_lp_packet(&packet_to_process, &mut buf_a2).unwrap();
let parsed_packet_a2 = parse_lp_packet(&buf_a2).unwrap();
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(lp_id, LpInput::ReceivePacket(parsed_packet_a2))
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_a2))
.expect("Responder ReceivePacket should produce an action")
.expect("Responder ReceivePacket failed");
@@ -1071,7 +1079,7 @@ mod tests {
}
// Responder still in Handshaking, waiting for final message
assert_eq!(
session_manager_2.get_state(lp_id).unwrap(),
session_manager_2.get_state(receiver_index).unwrap(),
LpStateBare::Handshaking,
"Responder state should still be Handshaking after sending second message"
);
@@ -1087,11 +1095,11 @@ mod tests {
.expect("Second Noise packet from B was missing");
let mut buf_b2 = BytesMut::new();
serialize_lp_packet(&packet_to_process, &mut buf_b2).unwrap();
let parsed_packet_b2 = parse_lp_packet(&buf_b2).unwrap();
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(lp_id, LpInput::ReceivePacket(parsed_packet_b2))
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_b2))
.expect("Initiator ReceivePacket should produce an action")
.expect("Initiator ReceivePacket failed");
@@ -1105,7 +1113,7 @@ mod tests {
}
// Initiator transitions to Transport after sending third message
assert_eq!(
session_manager_1.get_state(lp_id).unwrap(),
session_manager_1.get_state(receiver_index).unwrap(),
LpStateBare::Transport,
"Initiator state should be Transport after sending third message"
);
@@ -1121,11 +1129,11 @@ mod tests {
.expect("Third Noise packet from A was missing");
let mut buf_a3 = BytesMut::new();
serialize_lp_packet(&packet_to_process, &mut buf_a3).unwrap();
let parsed_packet_a3 = parse_lp_packet(&buf_a3).unwrap();
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(lp_id, LpInput::ReceivePacket(parsed_packet_a3))
.process_input(receiver_index, LpInput::ReceivePacket(parsed_packet_a3))
.expect("Responder final ReceivePacket should produce an action")
.expect("Responder final ReceivePacket failed");
@@ -1139,7 +1147,7 @@ mod tests {
);
}
assert_eq!(
session_manager_2.get_state(lp_id).unwrap(),
session_manager_2.get_state(receiver_index).unwrap(),
LpStateBare::Transport,
"Responder state should be Transport after processing third message"
);
@@ -1147,11 +1155,11 @@ mod tests {
// --- Verification ---
assert!(rounds < MAX_ROUNDS, "Handshake took too many rounds");
assert_eq!(
session_manager_1.get_state(lp_id).unwrap(),
session_manager_1.get_state(receiver_index).unwrap(),
LpStateBare::Transport
);
assert_eq!(
session_manager_2.get_state(lp_id).unwrap(),
session_manager_2.get_state(receiver_index).unwrap(),
LpStateBare::Transport
);
println!("Handshake simulation completed successfully via process_input.");
@@ -1164,7 +1172,7 @@ mod tests {
// --- A sends to B ---
println!(" A sends to B");
let action_a_send = session_manager_1
.process_input(lp_id, LpInput::SendData(plaintext_a_to_b.to_vec()))
.process_input(receiver_index, LpInput::SendData(plaintext_a_to_b.to_vec()))
.expect("A SendData should produce action")
.expect("A SendData failed");
@@ -1176,13 +1184,13 @@ mod tests {
// Simulate network
let mut buf_data_a = BytesMut::new();
serialize_lp_packet(&data_packet_a, &mut buf_data_a).unwrap();
let parsed_data_a = parse_lp_packet(&buf_data_a).unwrap();
serialize_lp_packet(&data_packet_a, &mut buf_data_a, None).unwrap();
let parsed_data_a = parse_lp_packet(&buf_data_a, None).unwrap();
// B receives
println!(" B receives from A");
let action_b_recv = session_manager_2
.process_input(lp_id, LpInput::ReceivePacket(parsed_data_a))
.process_input(receiver_index, LpInput::ReceivePacket(parsed_data_a))
.expect("B ReceivePacket (data) should produce action")
.expect("B ReceivePacket (data) failed");
@@ -1203,7 +1211,7 @@ mod tests {
// --- B sends to A ---
println!(" B sends to A");
let action_b_send = session_manager_2
.process_input(lp_id, LpInput::SendData(plaintext_b_to_a.to_vec()))
.process_input(receiver_index, LpInput::SendData(plaintext_b_to_a.to_vec()))
.expect("B SendData should produce action")
.expect("B SendData failed");
@@ -1217,13 +1225,13 @@ mod tests {
// Simulate network
let mut buf_data_b = BytesMut::new();
serialize_lp_packet(&data_packet_b, &mut buf_data_b).unwrap();
let parsed_data_b = parse_lp_packet(&buf_data_b).unwrap();
serialize_lp_packet(&data_packet_b, &mut buf_data_b, None).unwrap();
let parsed_data_b = parse_lp_packet(&buf_data_b, None).unwrap();
// A receives
println!(" A receives from B");
let action_a_recv = session_manager_1
.process_input(lp_id, LpInput::ReceivePacket(parsed_data_b))
.process_input(receiver_index, LpInput::ReceivePacket(parsed_data_b))
.expect("A ReceivePacket (data) should produce action")
.expect("A ReceivePacket (data) failed");
@@ -1245,7 +1253,7 @@ mod tests {
// --- 6. Replay Protection Test ---
println!("Testing data packet replay protection via process_input...");
let replay_result =
session_manager_1.process_input(lp_id, LpInput::ReceivePacket(data_packet_b_replay)); // Use cloned packet
session_manager_1.process_input(receiver_index, LpInput::ReceivePacket(data_packet_b_replay)); // Use cloned packet
assert!(replay_result.is_err(), "Replay should produce Err(...)");
let error = replay_result.err().unwrap();
@@ -1264,7 +1272,7 @@ mod tests {
let data_n = Bytes::from_static(b"Message N");
let action_send_n1 = session_manager_1
.process_input(lp_id, LpInput::SendData(data_n_plus_1.to_vec()))
.process_input(receiver_index, LpInput::SendData(data_n_plus_1.to_vec()))
.unwrap()
.unwrap();
let packet_n1 = match action_send_n1 {
@@ -1273,7 +1281,7 @@ mod tests {
};
let action_send_n = session_manager_1
.process_input(lp_id, LpInput::SendData(data_n.to_vec()))
.process_input(receiver_index, LpInput::SendData(data_n.to_vec()))
.unwrap()
.unwrap();
let packet_n = match action_send_n {
@@ -1285,7 +1293,7 @@ mod tests {
// B receives N+1 first
println!(" B receives N+1");
let action_recv_n1 = session_manager_2
.process_input(lp_id, LpInput::ReceivePacket(packet_n1))
.process_input(receiver_index, LpInput::ReceivePacket(packet_n1))
.unwrap()
.unwrap();
match action_recv_n1 {
@@ -1296,7 +1304,7 @@ mod tests {
// B receives N second (should work)
println!(" B receives N");
let action_recv_n = session_manager_2
.process_input(lp_id, LpInput::ReceivePacket(packet_n))
.process_input(receiver_index, LpInput::ReceivePacket(packet_n))
.unwrap()
.unwrap();
match action_recv_n {
@@ -1307,7 +1315,7 @@ mod tests {
// B tries to replay N (should fail)
println!(" B tries to replay N");
let replay_n_result =
session_manager_2.process_input(lp_id, LpInput::ReceivePacket(packet_n_replay));
session_manager_2.process_input(receiver_index, LpInput::ReceivePacket(packet_n_replay));
assert!(replay_n_result.is_err(), "Replay N should produce Err");
assert!(
matches!(replay_n_result.err().unwrap(), LpError::Replay(_)),
@@ -1320,18 +1328,18 @@ mod tests {
// A closes
let action_a_close = session_manager_1
.process_input(lp_id, LpInput::Close)
.process_input(receiver_index, LpInput::Close)
.expect("A Close should produce action")
.expect("A Close failed");
assert!(matches!(action_a_close, LpAction::ConnectionClosed));
assert_eq!(
session_manager_1.get_state(lp_id).unwrap(),
session_manager_1.get_state(receiver_index).unwrap(),
LpStateBare::Closed
);
// Further actions on A fail
let send_after_close_a =
session_manager_1.process_input(lp_id, LpInput::SendData(b"fail".to_vec()));
session_manager_1.process_input(receiver_index, LpInput::SendData(b"fail".to_vec()));
assert!(send_after_close_a.is_err());
assert!(matches!(
send_after_close_a.err().unwrap(),
@@ -1340,18 +1348,18 @@ mod tests {
// B closes
let action_b_close = session_manager_2
.process_input(lp_id, LpInput::Close)
.process_input(receiver_index, LpInput::Close)
.expect("B Close should produce action")
.expect("B Close failed");
assert!(matches!(action_b_close, LpAction::ConnectionClosed));
assert_eq!(
session_manager_2.get_state(lp_id).unwrap(),
session_manager_2.get_state(receiver_index).unwrap(),
LpStateBare::Closed
);
// Further actions on B fail
let send_after_close_b =
session_manager_2.process_input(lp_id, LpInput::SendData(b"fail".to_vec()));
session_manager_2.process_input(receiver_index, LpInput::SendData(b"fail".to_vec()));
assert!(send_after_close_b.is_err());
assert!(matches!(
send_after_close_b.err().unwrap(),
@@ -1360,15 +1368,15 @@ mod tests {
println!("Close test passed.");
// --- 9. Session Removal ---
assert!(session_manager_1.remove_state_machine(lp_id));
assert!(session_manager_1.remove_state_machine(receiver_index));
assert_eq!(session_manager_1.session_count(), 0);
assert!(!session_manager_1.state_machine_exists(lp_id));
assert!(!session_manager_1.state_machine_exists(receiver_index));
// B's session manager still has it until removed
assert!(session_manager_2.state_machine_exists(lp_id));
assert!(session_manager_2.remove_state_machine(lp_id));
assert!(session_manager_2.state_machine_exists(receiver_index));
assert!(session_manager_2.remove_state_machine(receiver_index));
assert_eq!(session_manager_2.session_count(), 0);
assert!(!session_manager_2.state_machine_exists(lp_id));
assert!(!session_manager_2.state_machine_exists(receiver_index));
println!("Session removal test passed.");
}
// ... other tests ...
+13 -3
View File
@@ -166,21 +166,22 @@ impl SessionManager {
pub fn create_session_state_machine(
&self,
receiver_index: u32,
local_ed25519_keypair: (&ed25519::PrivateKey, &ed25519::PublicKey),
remote_ed25519_key: &ed25519::PublicKey,
is_initiator: bool,
salt: &[u8; 32],
) -> Result<u32, LpError> {
let sm = LpStateMachine::new(
receiver_index,
is_initiator,
local_ed25519_keypair,
remote_ed25519_key,
salt,
)?;
let sm_id = sm.id()?;
self.state_machines.insert(sm_id, sm);
Ok(sm_id)
self.state_machines.insert(receiver_index, sm);
Ok(receiver_index)
}
/// Method to remove a state machine
@@ -215,9 +216,11 @@ mod tests {
let manager = SessionManager::new();
let ed25519_keypair = ed25519::KeyPair::from_secret([10u8; 32], 0);
let salt = [47u8; 32];
let receiver_index: u32 = 1001;
let sm_1_id = manager
.create_session_state_machine(
receiver_index,
(ed25519_keypair.private_key(), ed25519_keypair.public_key()),
ed25519_keypair.public_key(),
true,
@@ -237,9 +240,11 @@ mod tests {
let manager = SessionManager::new();
let ed25519_keypair = ed25519::KeyPair::from_secret([11u8; 32], 0);
let salt = [48u8; 32];
let receiver_index: u32 = 2002;
let sm_1_id = manager
.create_session_state_machine(
receiver_index,
(ed25519_keypair.private_key(), ed25519_keypair.public_key()),
ed25519_keypair.public_key(),
true,
@@ -265,6 +270,7 @@ mod tests {
let sm_1 = manager
.create_session_state_machine(
3001,
(
ed25519_keypair_1.private_key(),
ed25519_keypair_1.public_key(),
@@ -277,6 +283,7 @@ mod tests {
let sm_2 = manager
.create_session_state_machine(
3002,
(
ed25519_keypair_2.private_key(),
ed25519_keypair_2.public_key(),
@@ -289,6 +296,7 @@ mod tests {
let sm_3 = manager
.create_session_state_machine(
3003,
(
ed25519_keypair_3.private_key(),
ed25519_keypair_3.public_key(),
@@ -315,8 +323,10 @@ mod tests {
let manager = SessionManager::new();
let ed25519_keypair = ed25519::KeyPair::from_secret([15u8; 32], 0);
let salt = [50u8; 32];
let receiver_index: u32 = 4004;
let sm = manager.create_session_state_machine(
receiver_index,
(ed25519_keypair.private_key(), ed25519_keypair.public_key()),
ed25519_keypair.public_key(),
true,
+42 -28
View File
@@ -6,7 +6,6 @@
use crate::{
LpError,
keypair::{Keypair, PrivateKey as LpPrivateKey, PublicKey as LpPublicKey},
make_lp_id,
noise_protocol::NoiseError,
packet::LpPacket,
session::LpSession,
@@ -137,6 +136,7 @@ impl LpStateMachine {
///
/// # Arguments
///
/// * `receiver_index` - Client-proposed session identifier (random 4 bytes)
/// * `is_initiator` - Whether this side initiates the handshake
/// * `local_ed25519_keypair` - Ed25519 keypair for PSQ authentication and X25519 derivation
/// (from client identity key or gateway signing key)
@@ -148,6 +148,7 @@ impl LpStateMachine {
/// Returns `LpError::Ed25519RecoveryError` if Ed25519→X25519 conversion fails for the remote key.
/// Local private key conversion cannot fail.
pub fn new(
receiver_index: u32,
is_initiator: bool,
local_ed25519_keypair: (&ed25519::PrivateKey, &ed25519::PublicKey),
remote_ed25519_key: &ed25519::PublicKey,
@@ -161,7 +162,6 @@ impl LpStateMachine {
// The derived X25519 keys are used for:
// - Noise protocol ephemeral DH
// - PSQ ECDH baseline security (pre-quantum)
// - lp_id calculation (session identifier)
// Convert Ed25519 keys to X25519 for Noise protocol
let local_x25519_private = local_ed25519_keypair.0.to_x25519();
@@ -179,15 +179,13 @@ impl LpStateMachine {
let lp_public = LpPublicKey::from_bytes(local_x25519_public.as_bytes())?;
let lp_remote_public = LpPublicKey::from_bytes(remote_x25519_public.as_bytes())?;
// Create X25519 keypair for Noise and lp_id calculation
// Create X25519 keypair for Noise
let local_x25519_keypair = Keypair::from_keys(lp_private, lp_public);
// Calculate the shared lp_id using derived X25519 keys
let lp_id = make_lp_id(local_x25519_keypair.public_key(), &lp_remote_public);
// Create the session with both Ed25519 (for PSQ auth) and derived X25519 keys (for Noise)
// receiver_index is client-proposed, passed through directly
let session = LpSession::new(
lp_id,
receiver_index,
is_initiator,
local_ed25519_keypair,
local_x25519_keypair.private_key(),
@@ -252,8 +250,8 @@ impl LpStateMachine {
// --- KKTExchange State ---
(LpState::KKTExchange { session }, LpInput::ReceivePacket(packet)) => {
// Check if packet lp_id matches our session
if packet.header.session_id() != session.id() {
result_action = Some(Err(LpError::UnknownSessionId(packet.header.session_id())));
if packet.header.receiver_idx() != session.id() {
result_action = Some(Err(LpError::UnknownSessionId(packet.header.receiver_idx())));
LpState::KKTExchange { session }
} else {
use crate::message::LpMessage;
@@ -356,8 +354,8 @@ impl LpStateMachine {
// --- Handshaking State ---
(LpState::Handshaking { session }, LpInput::ReceivePacket(packet)) => {
// Check if packet lp_id matches our session
if packet.header.session_id() != session.id() {
result_action = Some(Err(LpError::UnknownSessionId(packet.header.session_id())));
if packet.header.receiver_idx() != session.id() {
result_action = Some(Err(LpError::UnknownSessionId(packet.header.receiver_idx())));
// Don't change state, return the original state variant
LpState::Handshaking { session }
} else {
@@ -454,8 +452,8 @@ impl LpStateMachine {
// --- Transport State ---
(LpState::Transport { session }, LpInput::ReceivePacket(packet)) => { // Needs mut session for marking counter
// Check if packet lp_id matches our session
if packet.header.session_id() != session.id() {
result_action = Some(Err(LpError::UnknownSessionId(packet.header.session_id())));
if packet.header.receiver_idx() != session.id() {
result_action = Some(Err(LpError::UnknownSessionId(packet.header.receiver_idx())));
// Remain in transport state
LpState::Transport { session }
} else {
@@ -605,7 +603,10 @@ mod tests {
// Test salt
let salt = [51u8; 32];
let receiver_index: u32 = 77777;
let initiator_sm = LpStateMachine::new(
receiver_index,
true,
(
ed25519_keypair_init.private_key(),
@@ -624,6 +625,7 @@ mod tests {
assert!(init_session.is_initiator());
let responder_sm = LpStateMachine::new(
receiver_index,
false,
(
ed25519_keypair_resp.private_key(),
@@ -641,8 +643,7 @@ mod tests {
let resp_session = responder_sm.session().unwrap();
assert!(!resp_session.is_initiator());
// Check lp_id is the same (derived internally from Ed25519 keys)
// Both state machines should have the same lp_id
// Check both state machines use the same receiver_index
assert_eq!(init_session.id(), resp_session.id());
}
@@ -654,9 +655,11 @@ mod tests {
// Test salt
let salt = [52u8; 32];
let receiver_index: u32 = 88888;
// Create state machines (already in ReadyToHandshake)
let mut initiator = LpStateMachine::new(
receiver_index,
true, // is_initiator
(
ed25519_keypair_init.private_key(),
@@ -668,6 +671,7 @@ mod tests {
.unwrap();
let mut responder = LpStateMachine::new(
receiver_index,
false, // is_initiator
(
ed25519_keypair_resp.private_key(),
@@ -678,8 +682,7 @@ mod tests {
)
.unwrap();
let lp_id = initiator.id().unwrap();
assert_eq!(lp_id, responder.id().unwrap());
assert_eq!(initiator.id().unwrap(), responder.id().unwrap());
// --- KKT Exchange ---
println!("--- Step 1: Initiator starts handshake (sends KKT request) ---");
@@ -695,9 +698,9 @@ mod tests {
"Initiator should be in KKTExchange"
);
assert_eq!(
kkt_request_packet.header.session_id(),
lp_id,
"KKT request packet has wrong lp_id"
kkt_request_packet.header.receiver_idx(),
receiver_index,
"KKT request packet has wrong receiver_index"
);
println!("--- Step 2: Responder starts handshake (waits for KKT) ---");
@@ -763,9 +766,9 @@ mod tests {
"Responder still Handshaking"
);
assert_eq!(
resp_packet_2.header.session_id(),
lp_id,
"Packet 2 has wrong lp_id"
resp_packet_2.header.receiver_idx(),
receiver_index,
"Packet 2 has wrong receiver_index"
);
println!("--- Step 6: Initiator receives Noise msg 2, sends Noise msg 3 ---");
@@ -780,9 +783,9 @@ mod tests {
"Initiator should be Transport"
);
assert_eq!(
init_packet_3.header.session_id(),
lp_id,
"Noise packet 3 has wrong lp_id"
init_packet_3.header.receiver_idx(),
receiver_index,
"Noise packet 3 has wrong receiver_index"
);
println!("--- Step 7: Responder receives Noise msg 3, completes handshake ---");
@@ -805,7 +808,7 @@ mod tests {
} else {
panic!("Initiator should send data packet");
};
assert_eq!(data_packet_1.header.session_id(), lp_id);
assert_eq!(data_packet_1.header.receiver_idx(), receiver_index);
println!("--- Step 9: Responder receives data ---");
let resp_actions_5 = responder.process_input(LpInput::ReceivePacket(data_packet_1));
@@ -824,7 +827,7 @@ mod tests {
} else {
panic!("Responder should send data packet");
};
assert_eq!(data_packet_2.header.session_id(), lp_id);
assert_eq!(data_packet_2.header.receiver_idx(), receiver_index);
println!("--- Step 11: Initiator receives data ---");
let init_actions_5 = initiator.process_input(LpInput::ReceivePacket(data_packet_2));
@@ -859,9 +862,11 @@ mod tests {
let ed25519_keypair_resp = ed25519::KeyPair::from_secret([21u8; 32], 1);
let salt = [53u8; 32];
let receiver_index: u32 = 99901;
// Create initiator state machine
let mut initiator = LpStateMachine::new(
receiver_index,
true,
(
ed25519_keypair_init.private_key(),
@@ -888,9 +893,11 @@ mod tests {
let ed25519_keypair_resp = ed25519::KeyPair::from_secret([23u8; 32], 1);
let salt = [54u8; 32];
let receiver_index: u32 = 99902;
// Create responder state machine
let mut responder = LpStateMachine::new(
receiver_index,
false,
(
ed25519_keypair_resp.private_key(),
@@ -917,9 +924,11 @@ mod tests {
let ed25519_keypair_resp = ed25519::KeyPair::from_secret([25u8; 32], 1);
let salt = [55u8; 32];
let receiver_index: u32 = 99903;
// Create both state machines
let mut initiator = LpStateMachine::new(
receiver_index,
true,
(
ed25519_keypair_init.private_key(),
@@ -931,6 +940,7 @@ mod tests {
.unwrap();
let mut responder = LpStateMachine::new(
receiver_index,
false,
(
ed25519_keypair_resp.private_key(),
@@ -979,9 +989,11 @@ mod tests {
let ed25519_keypair_resp = ed25519::KeyPair::from_secret([27u8; 32], 1);
let salt = [56u8; 32];
let receiver_index: u32 = 99904;
// Create initiator state machine
let mut initiator = LpStateMachine::new(
receiver_index,
true,
(
ed25519_keypair_init.private_key(),
@@ -1009,9 +1021,11 @@ mod tests {
let ed25519_keypair_resp = ed25519::KeyPair::from_secret([29u8; 32], 1);
let salt = [57u8; 32];
let receiver_index: u32 = 99905;
// Create initiator state machine
let mut initiator = LpStateMachine::new(
receiver_index,
true,
(
ed25519_keypair_init.private_key(),
+4 -67
View File
@@ -60,9 +60,6 @@ pub struct LpRegistrationResponse {
/// Allocated bandwidth in bytes
pub allocated_bandwidth: i64,
/// Session identifier for future reference
pub session_id: u32,
}
impl LpRegistrationRequest {
@@ -100,24 +97,22 @@ impl LpRegistrationRequest {
impl LpRegistrationResponse {
/// Create a success response with GatewayData
pub fn success(session_id: u32, allocated_bandwidth: i64, gateway_data: GatewayData) -> Self {
pub fn success(allocated_bandwidth: i64, gateway_data: GatewayData) -> Self {
Self {
success: true,
error: None,
gateway_data: Some(gateway_data),
allocated_bandwidth,
session_id,
}
}
/// Create an error response
pub fn error(session_id: u32, error: String) -> Self {
pub fn error(error: String) -> Self {
Self {
success: false,
error: Some(error),
gateway_data: None,
allocated_bandwidth: 0,
session_id,
}
}
}
@@ -153,13 +148,12 @@ mod tests {
let allocated_bandwidth = 1_000_000_000;
let response =
LpRegistrationResponse::success(session_id, allocated_bandwidth, gateway_data.clone());
LpRegistrationResponse::success(allocated_bandwidth, gateway_data.clone());
assert!(response.success);
assert!(response.error.is_none());
assert!(response.gateway_data.is_some());
assert_eq!(response.allocated_bandwidth, allocated_bandwidth);
assert_eq!(response.session_id, session_id);
let returned_gw_data = response
.gateway_data
@@ -172,72 +166,15 @@ mod tests {
#[test]
fn test_lp_registration_response_error() {
let session_id = 54321;
let error_msg = String::from("Insufficient bandwidth");
let response = LpRegistrationResponse::error(session_id, error_msg.clone());
let response = LpRegistrationResponse::error(error_msg.clone());
assert!(!response.success);
assert_eq!(response.error, Some(error_msg));
assert!(response.gateway_data.is_none());
assert_eq!(response.allocated_bandwidth, 0);
assert_eq!(response.session_id, session_id);
}
#[test]
fn test_lp_registration_response_serialize_deserialize_success() {
let gateway_data = create_test_gateway_data();
let original = LpRegistrationResponse::success(999, 5_000_000_000, gateway_data);
// Serialize
let serialized = bincode::serialize(&original).expect("Failed to serialize response");
// Deserialize
let deserialized: LpRegistrationResponse =
bincode::deserialize(&serialized).expect("Failed to deserialize response");
assert_eq!(deserialized.success, original.success);
assert_eq!(deserialized.error, original.error);
assert_eq!(
deserialized.allocated_bandwidth,
original.allocated_bandwidth
);
assert_eq!(deserialized.session_id, original.session_id);
assert!(deserialized.gateway_data.is_some());
}
#[test]
fn test_lp_registration_response_serialize_deserialize_error() {
let original = LpRegistrationResponse::error(777, String::from("Test error message"));
// Serialize
let serialized = bincode::serialize(&original).expect("Failed to serialize response");
// Deserialize
let deserialized: LpRegistrationResponse =
bincode::deserialize(&serialized).expect("Failed to deserialize response");
assert_eq!(deserialized.success, original.success);
assert_eq!(deserialized.error, original.error);
assert_eq!(deserialized.allocated_bandwidth, 0);
assert_eq!(deserialized.session_id, original.session_id);
assert!(deserialized.gateway_data.is_none());
}
#[test]
fn test_lp_registration_response_malformed_deserialize() {
// Create invalid bincode data
let invalid_data = vec![0xFF; 100];
// Attempt to deserialize
let result: Result<LpRegistrationResponse, _> = bincode::deserialize(&invalid_data);
assert!(
result.is_err(),
"Expected deserialization to fail for malformed data"
);
}
// ==================== RegistrationMode Tests ====================
#[test]
+217 -125
View File
@@ -5,7 +5,10 @@ use super::messages::LpRegistrationRequest;
use super::registration::process_registration;
use super::LpHandlerState;
use crate::error::GatewayError;
use nym_lp::{keypair::PublicKey, message::ForwardPacketData, LpMessage, LpPacket};
use nym_lp::{
codec::OuterAeadKey, keypair::PublicKey, message::ForwardPacketData, packet::LpHeader,
LpMessage, LpPacket,
};
use nym_metrics::{add_histogram_obs, inc};
use std::net::SocketAddr;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
@@ -91,9 +94,9 @@ impl LpConnectionHandler {
// State persists in LpHandlerState maps between connections
// ============================================================
// Step 1: Receive the packet
let packet = match self.receive_lp_packet().await {
Ok(p) => p,
// Step 1: Receive raw packet bytes and parse header only (for routing)
let (raw_bytes, header) = match self.receive_raw_packet().await {
Ok(result) => result,
Err(e) => {
inc!("lp_errors_receive_packet");
self.emit_lifecycle_metrics(false);
@@ -101,65 +104,94 @@ impl LpConnectionHandler {
}
};
let header = packet.header();
let session_id = header.session_id;
let receiver_idx = header.receiver_idx;
// Step 2: Get outer_aead_key based on receiver_idx
// Header is always cleartext for routing. Payload is encrypted after PSK.
// We lookup the session to get the key, then parse the full packet.
let outer_key: Option<OuterAeadKey> = if receiver_idx == nym_lp::BOOTSTRAP_RECEIVER_IDX {
// ClientHello - no encryption (PSK not yet derived)
None
} else if let Some(state_entry) = self.state.handshake_states.get(&receiver_idx) {
// Handshake in progress - check if PSK has been injected yet
state_entry
.value()
.state
.session()
.ok()
.and_then(|session| session.outer_aead_key())
} else if let Some(session_entry) = self.state.session_states.get(&receiver_idx) {
// Established session - should always have PSK
session_entry.value().state.outer_aead_key()
} else {
// Unknown session - will error during routing, parse cleartext
None
};
// Step 3: Parse full packet with outer AEAD key
let packet = nym_lp::codec::parse_lp_packet(&raw_bytes, outer_key.as_ref()).map_err(|e| {
inc!("lp_errors_parse_packet");
self.emit_lifecycle_metrics(false);
GatewayError::LpProtocolError(format!("Failed to parse LP packet: {}", e))
})?;
trace!(
"Received packet from {} (session_id={}, counter={})",
"Received packet from {} (receiver_idx={}, counter={}, encrypted={})",
self.remote_addr,
session_id,
header.counter
receiver_idx,
packet.header().counter,
outer_key.is_some()
);
// Step 2: Route packet based on session_id
if session_id == nym_lp::BOOTSTRAP_SESSION_ID {
// Step 4: Route packet based on receiver_idx
if receiver_idx == nym_lp::BOOTSTRAP_RECEIVER_IDX {
// ClientHello - first packet in handshake
self.handle_client_hello(packet).await
} else {
// Check if this is an in-progress handshake or established session
if self.state.handshake_states.contains_key(&session_id) {
if self.state.handshake_states.contains_key(&receiver_idx) {
// Handshake in progress
self.handle_handshake_packet(session_id, packet).await
} else if self.state.session_states.contains_key(&session_id) {
self.handle_handshake_packet(receiver_idx, packet).await
} else if self.state.session_states.contains_key(&receiver_idx) {
// Established session - transport mode
self.handle_transport_packet(session_id, packet).await
self.handle_transport_packet(receiver_idx, packet).await
} else {
// Unknown session - possibly stale or client error
warn!(
"Received packet for unknown session {} from {}",
session_id, self.remote_addr
receiver_idx, self.remote_addr
);
inc!("lp_errors_unknown_session");
self.emit_lifecycle_metrics(false);
Err(GatewayError::LpProtocolError(format!(
"Unknown session ID: {}",
session_id
receiver_idx
)))
}
}
}
/// Handle ClientHello packet (session_id=0, first packet)
/// Handle ClientHello packet (receiver_idx=0, first packet)
async fn handle_client_hello(&mut self, packet: LpPacket) -> Result<(), GatewayError> {
use nym_lp::state_machine::{LpInput, LpStateMachine};
use nym_lp::packet::LpHeader;
// Extract ClientHello data
let (_client_pubkey, client_ed25519_pubkey, salt) = match packet.message() {
let (receiver_index, client_ed25519_pubkey, salt) = match packet.message() {
LpMessage::ClientHello(hello_data) => {
// Validate timestamp
let timestamp = hello_data.extract_timestamp();
Self::validate_timestamp(timestamp, self.state.lp_config.timestamp_tolerance_secs)?;
// Extract keys
let client_pubkey = nym_lp::keypair::PublicKey::from_bytes(&hello_data.client_lp_public_key)
.map_err(|e| GatewayError::LpProtocolError(format!("Invalid client public key: {}", e)))?;
// Extract client-proposed receiver_index
let receiver_index = hello_data.receiver_index;
let client_ed25519_pubkey = nym_crypto::asymmetric::ed25519::PublicKey::from_bytes(
&hello_data.client_ed25519_public_key,
)
.map_err(|e| GatewayError::LpProtocolError(format!("Invalid client Ed25519 public key: {}", e)))?;
(client_pubkey, client_ed25519_pubkey, hello_data.salt)
(receiver_index, client_ed25519_pubkey, hello_data.salt)
}
other => {
inc!("lp_client_hello_failed");
@@ -171,10 +203,31 @@ impl LpConnectionHandler {
}
};
debug!("Processing ClientHello from {}", self.remote_addr);
debug!("Processing ClientHello from {} (proposed receiver_index={})", self.remote_addr, receiver_index);
// Create state machine for this handshake
// Collision check for client-proposed receiver_index
// Check both handshake_states (in-progress) and session_states (established)
if self.state.handshake_states.contains_key(&receiver_index)
|| self.state.session_states.contains_key(&receiver_index)
{
warn!("Receiver index collision: {} from {}", receiver_index, self.remote_addr);
inc!("lp_receiver_index_collision");
// Send Collision response to tell client to retry with new receiver_index
// No outer key - this is before PSK derivation
let collision_packet = LpPacket::new(
LpHeader::new(receiver_index, 0),
LpMessage::Collision,
);
self.send_lp_packet(&collision_packet, None).await?;
self.emit_lifecycle_metrics(true);
return Ok(());
}
// Create state machine for this handshake using client-proposed receiver_index
let mut state_machine = LpStateMachine::new(
receiver_index,
false, // responder
(
self.state.local_identity.private_key(),
@@ -188,14 +241,9 @@ impl LpConnectionHandler {
GatewayError::LpHandshakeError(format!("Failed to create state machine: {}", e))
})?;
// Get the computed session ID
let session_id = state_machine.session()
.map_err(|e| GatewayError::LpHandshakeError(format!("Failed to get session: {}", e)))?
.id();
debug!(
"Created handshake state for {} (session_id={})",
self.remote_addr, session_id
"Created handshake state for {} (receiver_index={})",
self.remote_addr, receiver_index
);
// Transition state machine to KKTExchange (responder waits for client's KKT request)
@@ -212,35 +260,42 @@ impl LpConnectionHandler {
// Responder (gateway) gets Ok but no packet to send - we just wait for client's next packet
}
// Store state machine for subsequent handshake packets (KKT request with session_id=X)
self.state.handshake_states.insert(session_id, super::TimestampedState::new(state_machine));
// Store state machine for subsequent handshake packets (KKT request with receiver_index=X)
self.state.handshake_states.insert(receiver_index, super::TimestampedState::new(state_machine));
debug!(
"Stored handshake state for {} (session_id={}) - waiting for KKT request",
self.remote_addr, session_id
"Stored handshake state for {} (receiver_index={}) - waiting for KKT request",
self.remote_addr, receiver_index
);
// NO packet sent - connection closes, client will send KKT request on new connection
// Send Ack to confirm ClientHello received (packet-per-connection model)
// No outer key - this is before PSK derivation
let ack_packet = LpPacket::new(
LpHeader::new(receiver_index, 0),
LpMessage::Ack,
);
self.send_lp_packet(&ack_packet, None).await?;
self.emit_lifecycle_metrics(true);
Ok(())
}
/// Handle handshake packet (session_id!=0, handshake not complete)
/// Handle handshake packet (receiver_idx!=0, handshake not complete)
async fn handle_handshake_packet(
&mut self,
session_id: u32,
receiver_idx: u32,
packet: LpPacket,
) -> Result<(), GatewayError> {
use nym_lp::state_machine::{LpInput, LpAction};
debug!(
"Processing handshake packet from {} (session_id={})",
self.remote_addr, session_id
"Processing handshake packet from {} (receiver_idx={})",
self.remote_addr, receiver_idx
);
// Get mutable reference to state machine
let mut state_entry = self.state.handshake_states.get_mut(&session_id).ok_or_else(|| {
GatewayError::LpProtocolError(format!("Handshake state not found for session {}", session_id))
let mut state_entry = self.state.handshake_states.get_mut(&receiver_idx).ok_or_else(|| {
GatewayError::LpProtocolError(format!("Handshake state not found for session {}", receiver_idx))
})?;
let state_machine = &mut state_entry.value_mut().state;
@@ -253,32 +308,39 @@ impl LpConnectionHandler {
})?
.map_err(|e| GatewayError::LpHandshakeError(format!("Handshake error: {}", e)))?;
let should_send_packet = match action {
// Get outer_aead_key from session (if PSK has been derived)
// PSK is derived after Noise msg 1 processing, so msg 2+ are encrypted
let should_send = match action {
LpAction::SendPacket(response_packet) => {
// Get key before dropping borrow
let outer_key = state_machine
.session()
.ok()
.and_then(|s| s.outer_aead_key());
drop(state_entry); // Release borrow before send
Some(response_packet)
Some((response_packet, outer_key))
}
LpAction::HandshakeComplete => {
info!(
"Handshake completed for {} (session_id={})",
self.remote_addr, session_id
"Handshake completed for {} (receiver_idx={})",
self.remote_addr, receiver_idx
);
// Extract session and move to session_states
drop(state_entry); // Release mutable borrow
let (_session_id, timestamped_state) = self.state.handshake_states.remove(&session_id)
let (_receiver_idx, timestamped_state) = self.state.handshake_states.remove(&receiver_idx)
.ok_or_else(|| GatewayError::LpHandshakeError("Failed to remove handshake state".to_string()))?;
let session = timestamped_state.state.into_session()
.map_err(|e| GatewayError::LpHandshakeError(format!("Failed to extract session: {}", e)))?;
self.state.session_states.insert(session_id, super::TimestampedState::new(session));
self.state.session_states.insert(receiver_idx, super::TimestampedState::new(session));
inc!("lp_handshakes_success");
// No response packet to send - HandshakeComplete means we're done
trace!("Moved session {} to transport mode", session_id);
trace!("Moved session {} to transport mode", receiver_idx);
None
}
other => {
@@ -289,34 +351,34 @@ impl LpConnectionHandler {
};
// Send response packet if needed
if let Some(packet) = should_send_packet {
self.send_lp_packet(&packet).await?;
trace!("Sent handshake response to {}", self.remote_addr);
if let Some((packet, outer_key)) = should_send {
self.send_lp_packet(&packet, outer_key.as_ref()).await?;
trace!("Sent handshake response to {} (encrypted={})", self.remote_addr, outer_key.is_some());
}
self.emit_lifecycle_metrics(true);
Ok(())
}
/// Handle transport packet (session_id!=0, session established)
/// Handle transport packet (receiver_idx!=0, session established)
///
/// This handles packets on established sessions, which can be either:
/// 1. LpRegistrationRequest - Client registering for dVPN/Mixnet access
/// 2. ForwardPacketData - Client forwarding packets to exit gateway (telescoping)
async fn handle_transport_packet(
&mut self,
session_id: u32,
receiver_idx: u32,
packet: LpPacket,
) -> Result<(), GatewayError> {
debug!(
"Processing transport packet from {} (session_id={})",
self.remote_addr, session_id
"Processing transport packet from {} (receiver_idx={})",
self.remote_addr, receiver_idx
);
// Get session and decrypt payload
let decrypted_bytes = {
let session_entry = self.state.session_states.get(&session_id).ok_or_else(|| {
GatewayError::LpProtocolError(format!("Session not found: {}", session_id))
let session_entry = self.state.session_states.get(&receiver_idx).ok_or_else(|| {
GatewayError::LpProtocolError(format!("Session not found: {}", receiver_idx))
})?;
// Update last activity timestamp
@@ -333,25 +395,25 @@ impl LpConnectionHandler {
// Try to deserialize as LpRegistrationRequest first (most common case after handshake)
if let Ok(request) = bincode::deserialize::<LpRegistrationRequest>(&decrypted_bytes) {
debug!(
"LP registration request from {} (session_id={}): mode={:?}",
self.remote_addr, session_id, request.mode
"LP registration request from {} (receiver_idx={}): mode={:?}",
self.remote_addr, receiver_idx, request.mode
);
return self.handle_registration_request(session_id, request).await;
return self.handle_registration_request(receiver_idx, request).await;
}
// Try to deserialize as ForwardPacketData (entry gateway forwarding to exit)
if let Ok(forward_data) = bincode::deserialize::<ForwardPacketData>(&decrypted_bytes) {
debug!(
"LP forward request from {} (session_id={}) to {}",
self.remote_addr, session_id, forward_data.target_lp_address
"LP forward request from {} (receiver_idx={}) to {}",
self.remote_addr, receiver_idx, forward_data.target_lp_address
);
return self.handle_forwarding_request(session_id, forward_data).await;
return self.handle_forwarding_request(receiver_idx, forward_data).await;
}
// Neither registration nor forwarding - unknown payload type
warn!(
"Unknown transport payload type from {} (session_id={})",
self.remote_addr, session_id
"Unknown transport payload type from {} (receiver_idx={})",
self.remote_addr, receiver_idx
);
inc!("lp_errors_unknown_payload_type");
self.emit_lifecycle_metrics(false);
@@ -363,16 +425,16 @@ impl LpConnectionHandler {
/// Handle registration request on an established session
async fn handle_registration_request(
&mut self,
session_id: u32,
receiver_idx: u32,
request: LpRegistrationRequest,
) -> Result<(), GatewayError> {
// Process registration (might modify state)
let response = process_registration(request, &self.state).await;
// Acquire session lock for encryption
let response_packet = {
let session_entry = self.state.session_states.get(&session_id).ok_or_else(|| {
GatewayError::LpProtocolError(format!("Session not found: {}", session_id))
// Acquire session lock for encryption and get outer AEAD key
let (response_packet, outer_key) = {
let session_entry = self.state.session_states.get(&receiver_idx).ok_or_else(|| {
GatewayError::LpProtocolError(format!("Session not found: {}", receiver_idx))
})?;
let session = &session_entry.value().state;
@@ -385,23 +447,27 @@ impl LpConnectionHandler {
GatewayError::LpProtocolError(format!("Failed to encrypt response: {}", e))
})?;
session.next_packet(encrypted_message).map_err(|e| {
let packet = session.next_packet(encrypted_message).map_err(|e| {
GatewayError::LpProtocolError(format!("Failed to create response packet: {}", e))
})?
})?;
// Get outer AEAD key for packet encryption
let outer_key = session.outer_aead_key();
(packet, outer_key)
};
// Send response
self.send_lp_packet(&response_packet).await?;
// Send response (encrypted with outer AEAD)
self.send_lp_packet(&response_packet, outer_key.as_ref()).await?;
if response.success {
info!(
"LP registration successful for {} (session_id={})",
self.remote_addr, response.session_id
"LP registration successful for {})",
self.remote_addr
);
} else {
warn!(
"LP registration failed for {} (session_id={}): {:?}",
self.remote_addr, response.session_id, response.error
"LP registration failed for {}: {:?}",
self.remote_addr, response.error
);
}
@@ -416,16 +482,16 @@ impl LpConnectionHandler {
/// Connection closes after response is sent (single-packet model).
async fn handle_forwarding_request(
&mut self,
session_id: u32,
receiver_idx: u32,
forward_data: ForwardPacketData,
) -> Result<(), GatewayError> {
// Forward the packet to the target gateway
let response_bytes = self.handle_forward_packet(forward_data).await?;
// Encrypt response for client
let response_packet = {
let session_entry = self.state.session_states.get(&session_id).ok_or_else(|| {
GatewayError::LpProtocolError(format!("Session not found: {}", session_id))
// Encrypt response for client and get outer AEAD key
let (response_packet, outer_key) = {
let session_entry = self.state.session_states.get(&receiver_idx).ok_or_else(|| {
GatewayError::LpProtocolError(format!("Session not found: {}", receiver_idx))
})?;
let session = &session_entry.value().state;
@@ -433,17 +499,21 @@ impl LpConnectionHandler {
GatewayError::LpProtocolError(format!("Failed to encrypt forward response: {}", e))
})?;
session.next_packet(encrypted_message).map_err(|e| {
let packet = session.next_packet(encrypted_message).map_err(|e| {
GatewayError::LpProtocolError(format!("Failed to create response packet: {}", e))
})?
})?;
// Get outer AEAD key for packet encryption
let outer_key = session.outer_aead_key();
(packet, outer_key)
};
// Send encrypted response to client
self.send_lp_packet(&response_packet).await?;
// Send encrypted response to client (encrypted with outer AEAD)
self.send_lp_packet(&response_packet, outer_key.as_ref()).await?;
debug!(
"LP forwarding completed for {} (session_id={})",
self.remote_addr, session_id
"LP forwarding completed for {} (receiver_idx={})",
self.remote_addr, receiver_idx
);
self.emit_lifecycle_metrics(true);
@@ -513,8 +583,10 @@ impl LpConnectionHandler {
),
GatewayError,
> {
// Receive first packet which should be ClientHello
let packet = self.receive_lp_packet().await?;
// Receive first packet which should be ClientHello (no outer encryption)
let (raw_bytes, _header) = self.receive_raw_packet().await?;
let packet = nym_lp::codec::parse_lp_packet(&raw_bytes, None)
.map_err(|e| GatewayError::LpProtocolError(format!("Failed to parse packet: {}", e)))?;
// Verify it's a ClientHello message
match packet.message() {
@@ -687,9 +759,12 @@ impl LpConnectionHandler {
Ok(response_buf)
}
/// Receive an LP packet from the stream with proper length-prefixed framing
async fn receive_lp_packet(&mut self) -> Result<LpPacket, GatewayError> {
use nym_lp::codec::parse_lp_packet;
/// Receive raw packet bytes and parse header only (for routing before session lookup).
///
/// Returns the raw packet bytes and parsed header. The caller should look up
/// the session to get outer_aead_key, then call `parse_lp_packet()` with the key.
async fn receive_raw_packet(&mut self) -> Result<(Vec<u8>, LpHeader), GatewayError> {
use nym_lp::codec::parse_lp_header_only;
// Read 4-byte length prefix (u32 big-endian)
let mut len_buf = [0u8; 4];
@@ -717,18 +792,29 @@ impl LpConnectionHandler {
// Track bytes received (4 byte header + packet data)
self.stats.record_bytes_received(4 + packet_len);
parse_lp_packet(&packet_buf)
.map_err(|e| GatewayError::LpProtocolError(format!("Failed to parse LP packet: {}", e)))
// Parse header only (for routing - header is always cleartext)
let header = parse_lp_header_only(&packet_buf)
.map_err(|e| GatewayError::LpProtocolError(format!("Failed to parse LP header: {}", e)))?;
Ok((packet_buf, header))
}
/// Send an LP packet over the stream with proper length-prefixed framing
async fn send_lp_packet(&mut self, packet: &LpPacket) -> Result<(), GatewayError> {
/// Send an LP packet over the stream with proper length-prefixed framing.
///
/// # Arguments
/// * `packet` - The LP packet to send
/// * `outer_key` - Optional outer AEAD key for encryption (None for cleartext, Some for encrypted)
async fn send_lp_packet(
&mut self,
packet: &LpPacket,
outer_key: Option<&OuterAeadKey>,
) -> Result<(), GatewayError> {
use bytes::BytesMut;
use nym_lp::codec::serialize_lp_packet;
// Serialize the packet first
// Serialize the packet (encrypted if outer_key provided)
let mut packet_buf = BytesMut::new();
serialize_lp_packet(packet, &mut packet_buf).map_err(|e| {
serialize_lp_packet(packet, &mut packet_buf, outer_key).map_err(|e| {
GatewayError::LpProtocolError(format!("Failed to serialize packet: {}", e))
})?;
@@ -840,7 +926,7 @@ mod tests {
packet: &LpPacket,
) -> Result<(), std::io::Error> {
let mut packet_buf = BytesMut::new();
serialize_lp_packet(packet, &mut packet_buf)
serialize_lp_packet(packet, &mut packet_buf, None)
.map_err(|e| std::io::Error::other(e.to_string()))?;
// Write length prefix
@@ -868,7 +954,7 @@ mod tests {
stream.read_exact(&mut packet_buf).await?;
// Parse packet
parse_lp_packet(&packet_buf).map_err(|e| std::io::Error::other(e.to_string()))
parse_lp_packet(&packet_buf, None).map_err(|e| std::io::Error::other(e.to_string()))
}
// ==================== Existing Tests ====================
@@ -957,7 +1043,7 @@ mod tests {
// ==================== Packet I/O Tests ====================
#[tokio::test]
async fn test_receive_lp_packet_valid() {
async fn test_receive_raw_packet_valid() {
use tokio::net::{TcpListener, TcpStream};
// Bind to localhost
@@ -969,7 +1055,11 @@ mod tests {
let (stream, remote_addr) = listener.accept().await.unwrap();
let state = create_minimal_test_state().await;
let mut handler = LpConnectionHandler::new(stream, remote_addr, state);
handler.receive_lp_packet().await
// Two-phase: receive raw bytes + header, then parse full packet
let (raw_bytes, header) = handler.receive_raw_packet().await?;
let packet = parse_lp_packet(&raw_bytes, None)
.map_err(|e| GatewayError::LpProtocolError(format!("Failed to parse packet: {}", e)))?;
Ok::<_, GatewayError>((header, packet))
});
// Connect as client
@@ -980,7 +1070,7 @@ mod tests {
LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 42,
receiver_idx: 42,
counter: 0,
},
LpMessage::Busy,
@@ -990,14 +1080,16 @@ mod tests {
.unwrap();
// Handler should receive and parse it correctly
let received = server_task.await.unwrap().unwrap();
let (header, received) = server_task.await.unwrap().unwrap();
assert_eq!(header.protocol_version, 1);
assert_eq!(header.receiver_idx, 42);
assert_eq!(received.header().protocol_version, 1);
assert_eq!(received.header().session_id, 42);
assert_eq!(received.header().receiver_idx, 42);
assert_eq!(received.header().counter, 0);
}
#[tokio::test]
async fn test_receive_lp_packet_exceeds_max_size() {
async fn test_receive_raw_packet_exceeds_max_size() {
use tokio::net::{TcpListener, TcpStream};
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
@@ -1007,7 +1099,7 @@ mod tests {
let (stream, remote_addr) = listener.accept().await.unwrap();
let state = create_minimal_test_state().await;
let mut handler = LpConnectionHandler::new(stream, remote_addr, state);
handler.receive_lp_packet().await
handler.receive_raw_packet().await
});
let mut client_stream = TcpStream::connect(addr).await.unwrap();
@@ -1043,12 +1135,12 @@ mod tests {
LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 99,
receiver_idx: 99,
counter: 5,
},
LpMessage::Busy,
);
handler.send_lp_packet(&packet).await
handler.send_lp_packet(&packet, None).await
});
let mut client_stream = TcpStream::connect(addr).await.unwrap();
@@ -1060,7 +1152,7 @@ mod tests {
let received = read_lp_packet_from_stream(&mut client_stream)
.await
.unwrap();
assert_eq!(received.header().session_id, 99);
assert_eq!(received.header().receiver_idx, 99);
assert_eq!(received.header().counter, 5);
}
@@ -1083,12 +1175,12 @@ mod tests {
LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 100,
receiver_idx: 100,
counter: 10,
},
LpMessage::Handshake(HandshakeData(handshake_data)),
);
handler.send_lp_packet(&packet).await
handler.send_lp_packet(&packet, None).await
});
let mut client_stream = TcpStream::connect(addr).await.unwrap();
@@ -1097,7 +1189,7 @@ mod tests {
let received = read_lp_packet_from_stream(&mut client_stream)
.await
.unwrap();
assert_eq!(received.header().session_id, 100);
assert_eq!(received.header().receiver_idx, 100);
assert_eq!(received.header().counter, 10);
match received.message() {
LpMessage::Handshake(data) => assert_eq!(data, &HandshakeData(expected_data)),
@@ -1124,12 +1216,12 @@ mod tests {
LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 200,
receiver_idx: 200,
counter: 20,
},
LpMessage::EncryptedData(EncryptedDataPayload(encrypted_payload)),
);
handler.send_lp_packet(&packet).await
handler.send_lp_packet(&packet, None).await
});
let mut client_stream = TcpStream::connect(addr).await.unwrap();
@@ -1138,7 +1230,7 @@ mod tests {
let received = read_lp_packet_from_stream(&mut client_stream)
.await
.unwrap();
assert_eq!(received.header().session_id, 200);
assert_eq!(received.header().receiver_idx, 200);
assert_eq!(received.header().counter, 20);
match received.message() {
LpMessage::EncryptedData(data) => {
@@ -1170,12 +1262,12 @@ mod tests {
LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 300,
receiver_idx: 300,
counter: 30,
},
LpMessage::ClientHello(hello_data),
);
handler.send_lp_packet(&packet).await
handler.send_lp_packet(&packet, None).await
});
let mut client_stream = TcpStream::connect(addr).await.unwrap();
@@ -1184,7 +1276,7 @@ mod tests {
let received = read_lp_packet_from_stream(&mut client_stream)
.await
.unwrap();
assert_eq!(received.header().session_id, 300);
assert_eq!(received.header().receiver_idx, 300);
assert_eq!(received.header().counter, 30);
match received.message() {
LpMessage::ClientHello(data) => {
@@ -1229,7 +1321,7 @@ mod tests {
LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 0,
receiver_idx: 0,
counter: 0,
},
LpMessage::ClientHello(hello_data.clone()),
@@ -1293,7 +1385,7 @@ mod tests {
LpHeader {
protocol_version: 1,
reserved: 0,
session_id: 0,
receiver_idx: 0,
counter: 0,
},
LpMessage::ClientHello(hello_data),
+7 -3
View File
@@ -19,10 +19,12 @@ impl LpGatewayHandshake {
/// Create a new responder (gateway side) handshake
///
/// # Arguments
/// * `receiver_index` - Client-proposed receiver_index (from ClientHello)
/// * `gateway_ed25519_keypair` - Gateway's Ed25519 identity keypair (for PSQ auth and X25519 derivation)
/// * `client_ed25519_public_key` - Client's Ed25519 public key (from ClientHello)
/// * `salt` - Salt from ClientHello (for PSK derivation)
pub fn new_responder(
receiver_index: u32,
gateway_ed25519_keypair: (
&nym_crypto::asymmetric::ed25519::PrivateKey,
&nym_crypto::asymmetric::ed25519::PublicKey,
@@ -31,6 +33,7 @@ impl LpGatewayHandshake {
salt: &[u8; 32],
) -> Result<Self, GatewayError> {
let state_machine = LpStateMachine::new(
receiver_index,
false, // responder
gateway_ed25519_keypair,
client_ed25519_public_key,
@@ -114,9 +117,9 @@ impl LpGatewayHandshake {
use bytes::BytesMut;
use nym_lp::codec::serialize_lp_packet;
// Serialize the packet first
// Serialize the packet first (None key during handshake phase)
let mut packet_buf = BytesMut::new();
serialize_lp_packet(packet, &mut packet_buf).map_err(|e| {
serialize_lp_packet(packet, &mut packet_buf, None).map_err(|e| {
GatewayError::LpProtocolError(format!("Failed to serialize packet: {}", e))
})?;
@@ -169,7 +172,8 @@ impl LpGatewayHandshake {
GatewayError::LpConnectionError(format!("Failed to read packet data: {}", e))
})?;
let packet = parse_lp_packet(&packet_buf)
// Parse packet (None key during handshake phase)
let packet = parse_lp_packet(&packet_buf, None)
.map_err(|e| GatewayError::LpProtocolError(format!("Failed to parse packet: {}", e)))?;
debug!("Received LP packet ({} bytes + 4 byte header)", packet_len);
+17 -21
View File
@@ -142,7 +142,7 @@ pub async fn process_registration(
if !request.validate_timestamp(30) {
warn!("LP registration failed: timestamp too old or too far in future");
inc!("lp_registration_failed_timestamp");
return LpRegistrationResponse::error(session_id, "Invalid timestamp".to_string());
return LpRegistrationResponse::error("Invalid timestamp".to_string());
}
// 2. Process based on mode
@@ -163,10 +163,10 @@ pub async fn process_registration(
error!("LP WireGuard peer registration failed: {}", e);
inc!("lp_registration_dvpn_failed");
inc!("lp_errors_wg_peer_registration");
return LpRegistrationResponse::error(
session_id,
format!("WireGuard peer registration failed: {}", e),
);
return LpRegistrationResponse::error(format!(
"WireGuard peer registration failed: {}",
e
));
}
};
@@ -196,19 +196,16 @@ pub async fn process_registration(
remove_err
);
}
return LpRegistrationResponse::error(
session_id,
format!("Credential verification failed: {}", e),
);
return LpRegistrationResponse::error(format!(
"Credential verification failed: {}",
e
));
}
};
info!(
"LP dVPN registration successful for session {} (client_id: {})",
session_id, client_id
);
info!("LP dVPN registration successful (client_id: {})", client_id);
inc!("lp_registration_dvpn_success");
LpRegistrationResponse::success(session_id, allocated_bandwidth, gateway_data)
LpRegistrationResponse::success(allocated_bandwidth, gateway_data)
}
RegistrationMode::Mixnet {
client_id: client_id_bytes,
@@ -244,18 +241,18 @@ pub async fn process_registration(
client_id, e
);
inc!("lp_registration_mixnet_failed");
return LpRegistrationResponse::error(
session_id,
format!("Credential verification failed: {}", e),
);
return LpRegistrationResponse::error(format!(
"Credential verification failed: {}",
e
));
}
};
// For mixnet mode, we don't have WireGuard data
// In the future, this would set up mixnet-specific state
info!(
"LP Mixnet registration successful for session {} (client_id: {})",
session_id, client_id
"LP Mixnet registration successful (client_id: {})",
client_id
);
inc!("lp_registration_mixnet_success");
LpRegistrationResponse {
@@ -263,7 +260,6 @@ pub async fn process_registration(
error: None,
gateway_data: None,
allocated_bandwidth,
session_id,
}
}
};
@@ -248,6 +248,7 @@ impl LpRegistrationClient {
self.local_ed25519_keypair.public_key().to_bytes(),
);
let salt = client_hello_data.salt;
let receiver_index = client_hello_data.receiver_index;
tracing::trace!(
"Generated ClientHello with timestamp: {}",
@@ -256,7 +257,7 @@ impl LpRegistrationClient {
// Step 3: Send ClientHello as first packet (before Noise handshake)
let client_hello_header = nym_lp::packet::LpHeader::new(
nym_lp::BOOTSTRAP_SESSION_ID, // session_id not yet established
nym_lp::BOOTSTRAP_RECEIVER_IDX, // session_id not yet established
0, // counter starts at 0
);
let client_hello_packet = nym_lp::LpPacket::new(
@@ -269,6 +270,7 @@ impl LpRegistrationClient {
// Step 4: 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.local_ed25519_keypair.private_key(),
@@ -352,9 +354,9 @@ impl LpRegistrationClient {
/// # Errors
/// Returns an error if serialization or network transmission fails.
async fn send_packet(stream: &mut TcpStream, packet: &LpPacket) -> Result<()> {
// Serialize the packet
// During handshake, outer AEAD is not used (PSK not yet established)
let mut packet_buf = BytesMut::new();
serialize_lp_packet(packet, &mut packet_buf)
serialize_lp_packet(packet, &mut packet_buf, None)
.map_err(|e| LpClientError::Transport(format!("Failed to serialize packet: {}", e)))?;
// Send 4-byte length prefix (u32 big-endian)
@@ -416,8 +418,8 @@ impl LpRegistrationClient {
.await
.map_err(|e| LpClientError::Transport(format!("Failed to read packet data: {}", e)))?;
// Parse the packet
let packet = parse_lp_packet(&packet_buf)
// During handshake, outer AEAD is not used (PSK not yet established)
let packet = parse_lp_packet(&packet_buf, None)
.map_err(|e| LpClientError::Transport(format!("Failed to parse packet: {}", e)))?;
tracing::trace!("Received LP packet ({} bytes + 4 byte header)", packet_len);
@@ -705,9 +707,8 @@ impl LpRegistrationClient {
})?;
tracing::debug!(
"Received registration response: success={}, session_id={}",
"Received registration response: success={}",
response.success,
response.session_id
);
// 5. Validate and extract GatewayData
@@ -727,8 +728,7 @@ impl LpRegistrationClient {
})?;
tracing::info!(
"LP registration successful! Session ID: {}, Allocated bandwidth: {} bytes",
response.session_id,
"LP registration successful! Allocated bandwidth: {} bytes",
response.allocated_bandwidth
);
@@ -24,7 +24,7 @@ use bytes::BytesMut;
use nym_bandwidth_controller::BandwidthTicketProvider;
use nym_credentials_interface::TicketType;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_lp::codec::{parse_lp_packet, serialize_lp_packet};
use nym_lp::codec::{parse_lp_packet, serialize_lp_packet, OuterAeadKey};
use nym_lp::state_machine::{LpAction, LpInput, LpStateMachine};
use nym_lp::{LpMessage, LpPacket};
use nym_registration_common::{GatewayData, LpRegistrationRequest, LpRegistrationResponse};
@@ -135,6 +135,7 @@ impl NestedLpSession {
self.client_keypair.public_key().to_bytes(),
);
let salt = client_hello_data.salt;
let receiver_index = client_hello_data.receiver_index;
tracing::trace!(
"Generated ClientHello for exit gateway (timestamp: {})",
@@ -151,8 +152,8 @@ impl NestedLpSession {
LpMessage::ClientHello(client_hello_data),
);
// Serialize and forward ClientHello
let client_hello_bytes = Self::serialize_packet(&client_hello_packet)?;
// Serialize and forward ClientHello (no state machine yet, no outer key)
let client_hello_bytes = Self::serialize_packet(&client_hello_packet, None)?;
let _response_bytes = outer_client
.send_forward_packet(
self.exit_identity,
@@ -165,6 +166,7 @@ impl NestedLpSession {
// Step 4: Create state machine for exit gateway handshake
let mut state_machine = LpStateMachine::new(
receiver_index,
true, // is_initiator
(
self.client_keypair.private_key(),
@@ -179,7 +181,9 @@ impl NestedLpSession {
match action? {
LpAction::SendPacket(packet) => {
tracing::trace!("Sending initial handshake packet to exit");
let packet_bytes = Self::serialize_packet(&packet)?;
// Get outer key (None before PSK derivation)
let outer_key = state_machine.session().ok().and_then(|s| s.outer_aead_key());
let packet_bytes = Self::serialize_packet(&packet, outer_key.as_ref())?;
let response_bytes = outer_client
.send_forward_packet(
self.exit_identity,
@@ -189,7 +193,8 @@ impl NestedLpSession {
.await?;
// Parse response and feed to state machine
let response_packet = Self::parse_packet(&response_bytes)?;
let outer_key = state_machine.session().ok().and_then(|s| s.outer_aead_key());
let response_packet = Self::parse_packet(&response_bytes, outer_key.as_ref())?;
tracing::trace!("Received handshake response from exit");
// Process response through state machine
@@ -200,7 +205,8 @@ impl NestedLpSession {
LpAction::SendPacket(response_packet) => {
// Send response packet
tracing::trace!("Sending handshake response to exit");
let packet_bytes = Self::serialize_packet(&response_packet)?;
let outer_key = state_machine.session().ok().and_then(|s| s.outer_aead_key());
let packet_bytes = Self::serialize_packet(&response_packet, outer_key.as_ref())?;
let response_bytes = outer_client
.send_forward_packet(
self.exit_identity,
@@ -219,7 +225,8 @@ impl NestedLpSession {
}
// Process the response from exit gateway
let response_packet = Self::parse_packet(&response_bytes)?;
let outer_key = state_machine.session().ok().and_then(|s| s.outer_aead_key());
let response_packet = Self::parse_packet(&response_bytes, outer_key.as_ref())?;
if let Some(action) = state_machine
.process_input(LpInput::ReceivePacket(response_packet))
{
@@ -249,6 +256,7 @@ impl NestedLpSession {
LpAction::KKTComplete => {
tracing::info!("KKT exchange completed with exit, starting Noise");
// After KKT completes, initiator must send first Noise handshake message
// PSK is now available, so outer AEAD key can be used
let noise_msg = state_machine
.session()?
.prepare_handshake_message()
@@ -259,7 +267,8 @@ impl NestedLpSession {
})??;
let noise_packet = state_machine.session()?.next_packet(noise_msg)?;
tracing::trace!("Sending first Noise handshake message to exit");
let packet_bytes = Self::serialize_packet(&noise_packet)?;
let outer_key = state_machine.session().ok().and_then(|s| s.outer_aead_key());
let packet_bytes = Self::serialize_packet(&noise_packet, outer_key.as_ref())?;
let response_bytes = outer_client
.send_forward_packet(
self.exit_identity,
@@ -269,7 +278,8 @@ impl NestedLpSession {
.await?;
// Process the Noise response from exit gateway
let response_packet = Self::parse_packet(&response_bytes)?;
let outer_key = state_machine.session().ok().and_then(|s| s.outer_aead_key());
let response_packet = Self::parse_packet(&response_bytes, outer_key.as_ref())?;
if let Some(action) = state_machine
.process_input(LpInput::ReceivePacket(response_packet))
{
@@ -283,7 +293,8 @@ impl NestedLpSession {
}
LpAction::SendPacket(final_packet) => {
tracing::trace!("Sending final handshake packet to exit");
let packet_bytes = Self::serialize_packet(&final_packet)?;
let outer_key = state_machine.session().ok().and_then(|s| s.outer_aead_key());
let packet_bytes = Self::serialize_packet(&final_packet, outer_key.as_ref())?;
let _ = outer_client
.send_forward_packet(
self.exit_identity,
@@ -419,9 +430,11 @@ impl NestedLpSession {
})?;
// Step 7: Send the encrypted packet via forwarding
// Get outer key for AEAD encryption (PSK is available after handshake)
let outer_key = state_machine.session().ok().and_then(|s| s.outer_aead_key());
let response_bytes = match action {
LpAction::SendPacket(packet) => {
let packet_bytes = Self::serialize_packet(&packet)?;
let packet_bytes = Self::serialize_packet(&packet, outer_key.as_ref())?;
outer_client
.send_forward_packet(
self.exit_identity,
@@ -441,7 +454,8 @@ impl NestedLpSession {
tracing::trace!("Received registration response from exit gateway");
// Step 8: Parse response bytes to LP packet
let response_packet = Self::parse_packet(&response_bytes)?;
let outer_key = state_machine.session().ok().and_then(|s| s.outer_aead_key());
let response_packet = Self::parse_packet(&response_bytes, outer_key.as_ref())?;
// Step 9: Decrypt via state machine
let action = state_machine
@@ -477,9 +491,8 @@ impl NestedLpSession {
})?;
tracing::debug!(
"Received registration response from exit: success={}, session_id={}",
"Received registration response from exit: success={}",
response.success,
response.session_id
);
// Step 12: Validate and extract GatewayData
@@ -499,8 +512,7 @@ impl NestedLpSession {
})?;
tracing::info!(
"Exit gateway registration successful! Session ID: {}, Allocated bandwidth: {} bytes",
response.session_id,
"Exit gateway registration successful! Allocated bandwidth: {} bytes",
response.allocated_bandwidth
);
@@ -517,9 +529,10 @@ impl NestedLpSession {
///
/// # Errors
/// Returns an error if serialization fails
fn serialize_packet(packet: &LpPacket) -> Result<Vec<u8>> {
fn serialize_packet(packet: &LpPacket, outer_key: Option<&OuterAeadKey>) -> Result<Vec<u8>> {
let mut buf = BytesMut::new();
serialize_lp_packet(packet, &mut buf).map_err(|e| {
// Use outer AEAD key when available (after PSK derivation)
serialize_lp_packet(packet, &mut buf, outer_key).map_err(|e| {
LpClientError::Transport(format!("Failed to serialize LP packet: {}", e))
})?;
Ok(buf.to_vec())
@@ -535,8 +548,9 @@ impl NestedLpSession {
///
/// # Errors
/// Returns an error if parsing fails
fn parse_packet(bytes: &[u8]) -> Result<LpPacket> {
parse_lp_packet(bytes).map_err(|e| {
fn parse_packet(bytes: &[u8], outer_key: Option<&OuterAeadKey>) -> Result<LpPacket> {
// Use outer AEAD key when available (after PSK derivation)
parse_lp_packet(bytes, outer_key).map_err(|e| {
LpClientError::Transport(format!("Failed to parse LP packet: {}", e))
})
}
@@ -9,7 +9,7 @@
use super::error::{LpClientError, Result};
use bytes::BytesMut;
use nym_lp::LpPacket;
use nym_lp::codec::{parse_lp_packet, serialize_lp_packet};
use nym_lp::codec::{parse_lp_packet, serialize_lp_packet, OuterAeadKey};
use nym_lp::state_machine::{LpAction, LpInput, LpStateBare, LpStateMachine};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
@@ -196,9 +196,15 @@ impl LpTransport {
///
/// Format: 4-byte big-endian u32 length + packet bytes
async fn send_packet(&mut self, packet: &LpPacket) -> Result<()> {
// Serialize the packet
// Get outer_aead_key from session for AEAD encryption
let outer_key: Option<OuterAeadKey> = self
.state_machine
.session()
.ok()
.and_then(|s| s.outer_aead_key());
let mut packet_buf = BytesMut::new();
serialize_lp_packet(packet, &mut packet_buf)
serialize_lp_packet(packet, &mut packet_buf, outer_key.as_ref())
.map_err(|e| LpClientError::Transport(format!("Failed to serialize packet: {}", e)))?;
// Send 4-byte length prefix (u32 big-endian)
@@ -257,8 +263,14 @@ impl LpTransport {
.await
.map_err(|e| LpClientError::Transport(format!("Failed to read packet data: {}", e)))?;
// Parse the packet
let packet = parse_lp_packet(&packet_buf)
// Get outer_aead_key from session for AEAD decryption
let outer_key: Option<OuterAeadKey> = self
.state_machine
.session()
.ok()
.and_then(|s| s.outer_aead_key());
let packet = parse_lp_packet(&packet_buf, outer_key.as_ref())
.map_err(|e| LpClientError::Transport(format!("Failed to parse packet: {}", e)))?;
tracing::trace!("Received LP packet ({} bytes + 4 byte header)", packet_len);