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:
Generated
+1
@@ -6591,6 +6591,7 @@ dependencies = [
|
||||
"bincode",
|
||||
"bs58",
|
||||
"bytes",
|
||||
"chacha20poly1305",
|
||||
"criterion",
|
||||
"dashmap",
|
||||
"libcrux-kem",
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 ...
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user