Implement unified packet format with outer header first
Restructure LP packet format so cleartext fields (receiver_idx, counter) are always first, enabling trivial header parsing for routing before session lookup. Protocol version and reserved fields are now encrypted in the inner payload for encrypted packets. Wire format change: - Before: proto(1B) + reserved(3B) + receiver_idx(4B) + counter(8B) - After: receiver_idx(4B) + counter(8B) | proto(1B) + reserved(3B) + ... Key changes: - Add OuterHeader struct (12 bytes) for routing/replay protection - Update serialize_lp_packet/parse_lp_packet for unified format - parse_lp_header_only now returns OuterHeader - Gateway handler uses OuterHeader for session lookup - Update DESIGN.md with new wire format diagrams Security improvement: Only receiver_idx and counter visible after PSK establishment (was also exposing protocol version and reserved).
This commit is contained in:
+64
-30
@@ -11,25 +11,41 @@ The Lewes Protocol (LP) provides authenticated, encrypted sessions with replay p
|
||||
|
||||
## Packet Structure
|
||||
|
||||
### Unified Format (v2)
|
||||
|
||||
All packets share the same outer structure - cleartext fields are always first:
|
||||
|
||||
```
|
||||
┌─────────┬──────────┬────────────────┬─────────┬─────────────────────┬─────────┐
|
||||
│ version │ reserved │ receiver_index │ counter │ payload │ trailer │
|
||||
│ 1B │ 3B │ 4B │ 8B │ variable │ 16B │
|
||||
└─────────┴──────────┴────────────────┴─────────┴─────────────────────┴─────────┘
|
||||
16B header 16B
|
||||
┌────────────────┬─────────┬─────────┬──────────┬─────────────────────┬─────────┐
|
||||
│ receiver_index │ counter │ version │ reserved │ payload │ trailer │
|
||||
│ 4B │ 8B │ 1B │ 3B │ variable │ 16B │
|
||||
└────────────────┴─────────┴─────────┴──────────┴─────────────────────┴─────────┘
|
||||
│←── 12B outer header ────┤│←── inner (cleartext or encrypted) ──────┤│─ 16B ──┤
|
||||
```
|
||||
|
||||
**Total overhead:** 32 bytes (16B header + 16B trailer)
|
||||
**Total overhead:** 32 bytes (12B outer + 4B inner prefix + 16B trailer)
|
||||
|
||||
Key properties:
|
||||
- **Outer header** (12 bytes): Always cleartext, used for routing before session lookup
|
||||
- **Inner content**: Cleartext before PSK, encrypted after PSK
|
||||
- **No disambiguation needed**: Format is identical for both modes
|
||||
|
||||
### Field Descriptions
|
||||
|
||||
**Outer Header** (always cleartext, 12 bytes):
|
||||
|
||||
| Field | Size | Description |
|
||||
|-------|------|-------------|
|
||||
| receiver_index | 4 bytes | Session identifier, proposed by client (routing key) |
|
||||
| counter | 8 bytes | Monotonic counter, used as AEAD nonce and for replay protection |
|
||||
|
||||
**Inner Content** (cleartext or encrypted):
|
||||
|
||||
| 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) |
|
||||
| payload | variable | Message type (2B) + content |
|
||||
| trailer | 16 bytes | Zeros (no PSK) or AEAD Poly1305 tag (with PSK) |
|
||||
|
||||
### Wire Format
|
||||
@@ -54,10 +70,16 @@ Length-prefixed over TCP:
|
||||
| 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 |
|
||||
| Ack | 0x0008 | Gateway confirms receipt of message |
|
||||
|
||||
### Planned Message Types (not yet implemented)
|
||||
|
||||
| Type | Value | Description |
|
||||
|------|-------|-------------|
|
||||
| SubsessionRequest | 0x0009 | Client requests new subsession |
|
||||
| SubsessionKK1 | 0x000A | KK handshake msg 1 (responder → initiator) |
|
||||
| SubsessionKK2 | 0x000B | KK handshake msg 2 (initiator → responder) |
|
||||
| SubsessionReady | 0x000C | Subsession established confirmation |
|
||||
|
||||
## Receiver Index
|
||||
|
||||
@@ -91,6 +113,7 @@ As soon as PSK is derived (after processing Noise msg 1 with PSQ), all subsequen
|
||||
| Packet | PSK Available | Header | Payload | Trailer |
|
||||
|--------|---------------|--------|---------|---------|
|
||||
| ClientHello | No | Clear | Clear | Zeros |
|
||||
| Ack | No | Clear | Clear | Zeros |
|
||||
| KKTRequest | No | Clear | Clear | Zeros |
|
||||
| KKTResponse | No | Clear | Clear | Zeros |
|
||||
| Noise msg 1 | No | Clear | Clear | Zeros |
|
||||
@@ -104,38 +127,44 @@ As soon as PSK is derived (after processing Noise msg 1 with PSQ), all subsequen
|
||||
- **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)
|
||||
- **AAD**: receiver_index ‖ counter (12 bytes) - the outer header
|
||||
- **Encrypted**: version ‖ reserved ‖ message_type ‖ content
|
||||
|
||||
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 ──────────────────────────────────────┤
|
||||
┌────────────────┬─────────┬─────────┬──────────┬─────────────────────┬─────────┐
|
||||
│ receiver_index │ counter │ version │ reserved │ payload │ 00...00 │
|
||||
│ │ │ │ │ (plaintext) │ │
|
||||
└────────────────┴─────────┴─────────┴──────────┴─────────────────────┴─────────┘
|
||||
│←── 12B outer ──────────┤│←────────────── cleartext inner ──────────┤│─zeros──┤
|
||||
```
|
||||
|
||||
### After PSK
|
||||
|
||||
```
|
||||
┌─────────┬──────────┬────────────────┬─────────┬─────────────────────┬─────────┐
|
||||
│ version │ reserved │ receiver_index │ counter │ payload │ tag │
|
||||
│ │ │ │ │ (encrypted) │ │
|
||||
└─────────┴──────────┴────────────────┴─────────┴─────────────────────┴─────────┘
|
||||
│←───────── cleartext (authenticated via AAD) ─────────┤│← encrypted ─┤│─ auth ─┤
|
||||
┌────────────────┬─────────┬─────────┬──────────┬─────────────────────┬─────────┐
|
||||
│ receiver_index │ counter │ version │ reserved │ payload │ tag │
|
||||
│ │ │ (enc) │ (enc) │ (encrypted) │ │
|
||||
└────────────────┴─────────┴─────────┴──────────┴─────────────────────┴─────────┘
|
||||
│←── 12B outer (AAD) ────┤│←────────── encrypted inner ──────────────┤│─ tag ──┤
|
||||
```
|
||||
|
||||
## Handshake Flow
|
||||
|
||||
Each arrow represents a separate TCP connection (packet-per-connection model).
|
||||
|
||||
```
|
||||
Client Gateway
|
||||
│ │
|
||||
│ [hdr][ClientHello][zeros] │
|
||||
│──────────────────────────────────────►│ store state[receiver_index]
|
||||
│ │
|
||||
│ [hdr][Ack][zeros] │
|
||||
│◄──────────────────────────────────────│ confirm ClientHello
|
||||
│ │
|
||||
│ [hdr][KKTRequest][zeros] │
|
||||
│──────────────────────────────────────►│
|
||||
│ │
|
||||
@@ -299,18 +328,23 @@ SubsessionReadyData {
|
||||
|
||||
### Always Visible to Observer
|
||||
|
||||
- Version (1 byte)
|
||||
- Reserved (3 bytes)
|
||||
Only the outer header (12 bytes) is visible after PSK establishment:
|
||||
|
||||
- Receiver index (4 bytes) - opaque, unlinkable to identity
|
||||
- Counter (8 bytes) - reveals packet ordering
|
||||
- Packet size
|
||||
|
||||
Note: Before PSK, version, reserved, and message type are also visible.
|
||||
|
||||
### Protected After PSK
|
||||
|
||||
- Header integrity (authenticated via AEAD AAD)
|
||||
- Payload confidentiality (encrypted)
|
||||
- Message type (hidden)
|
||||
- Application data (double encrypted)
|
||||
- Outer header integrity (authenticated via AEAD AAD)
|
||||
- Inner content confidentiality (encrypted):
|
||||
- Protocol version
|
||||
- Reserved field
|
||||
- Message type
|
||||
- Payload
|
||||
- Application data (double encrypted: outer AEAD + inner Noise)
|
||||
|
||||
### Cryptographic Guarantees
|
||||
|
||||
|
||||
+132
-85
@@ -6,8 +6,14 @@ use crate::message::{
|
||||
ClientHelloData, EncryptedDataPayload, ForwardPacketData, HandshakeData, KKTRequestData,
|
||||
KKTResponseData, LpMessage, MessageType,
|
||||
};
|
||||
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
|
||||
use crate::packet::{LpHeader, LpPacket, OuterHeader, TRAILER_LEN};
|
||||
use bytes::{BufMut, BytesMut};
|
||||
|
||||
/// Size of outer header (receiver_idx + counter) - always cleartext
|
||||
pub const OUTER_HEADER_SIZE: usize = OuterHeader::SIZE; // 12 bytes
|
||||
|
||||
/// Size of inner prefix (proto + reserved) - cleartext or encrypted depending on mode
|
||||
const INNER_PREFIX_SIZE: usize = 4; // proto(1) + reserved(3)
|
||||
use chacha20poly1305::{
|
||||
aead::{AeadInPlace, KeyInit},
|
||||
ChaCha20Poly1305, Key, Nonce, Tag,
|
||||
@@ -135,28 +141,28 @@ fn parse_message_from_type_and_content(
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse only the LP header from raw packet bytes.
|
||||
/// Parse only the outer 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()`.
|
||||
/// Used for routing before session lookup. The outer header (receiver_idx + counter)
|
||||
/// is always cleartext at bytes 0-12 in the unified packet format.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `src` - Raw packet bytes (at least LpHeader::SIZE bytes)
|
||||
/// * `src` - Raw packet bytes (at least OuterHeader::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])
|
||||
/// * `LpError::InsufficientBufferSize` - Packet too small for outer header
|
||||
pub fn parse_lp_header_only(src: &[u8]) -> Result<OuterHeader, LpError> {
|
||||
OuterHeader::parse(src)
|
||||
}
|
||||
|
||||
/// 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).
|
||||
/// ## Unified Packet Format
|
||||
///
|
||||
/// Both cleartext and encrypted packets have the same structure:
|
||||
/// - Outer header (12B): receiver_idx(4) + counter(8) - always cleartext
|
||||
/// - Inner payload: proto(1) + reserved(3) + msg_type(2) + content - cleartext or encrypted
|
||||
/// - Trailer (16B): zeros (cleartext) or AEAD tag (encrypted)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `src` - Raw packet bytes
|
||||
@@ -169,40 +175,61 @@ 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;
|
||||
// Minimum size check: OuterHeader + InnerPrefix + MsgType + Trailer (for 0-payload message)
|
||||
// 12 + 4 + 2 + 16 = 34 bytes
|
||||
let min_size = OUTER_HEADER_SIZE + INNER_PREFIX_SIZE + 2 + TRAILER_LEN;
|
||||
if src.len() < min_size {
|
||||
return Err(LpError::InsufficientBufferSize);
|
||||
}
|
||||
|
||||
// Parse LpHeader (always cleartext for routing)
|
||||
let header = LpHeader::parse(&src[..LpHeader::SIZE])?;
|
||||
// Parse outer header (always cleartext at bytes 0-12)
|
||||
let outer_header = OuterHeader::parse(src)?;
|
||||
|
||||
// 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..]);
|
||||
|
||||
// Payload is everything between header and trailer
|
||||
let payload_bytes = &src[LpHeader::SIZE..trailer_start];
|
||||
// Inner payload is everything between outer header and trailer
|
||||
let inner_bytes = &src[OUTER_HEADER_SIZE..trailer_start];
|
||||
|
||||
// Handle decryption if outer key provided
|
||||
let (message_type_raw, message_content) = match outer_key {
|
||||
match outer_key {
|
||||
None => {
|
||||
// Cleartext mode - parse directly
|
||||
if payload_bytes.len() < 2 {
|
||||
// Cleartext mode - parse inner directly
|
||||
// Inner format: proto(1) + reserved(3) + msg_type(2) + content
|
||||
if inner_bytes.len() < INNER_PREFIX_SIZE + 2 {
|
||||
return Err(LpError::InsufficientBufferSize);
|
||||
}
|
||||
let msg_type = u16::from_le_bytes([payload_bytes[0], payload_bytes[1]]);
|
||||
(msg_type, &payload_bytes[2..])
|
||||
|
||||
let protocol_version = inner_bytes[0];
|
||||
// reserved bytes [1..4] are ignored
|
||||
let msg_type = u16::from_le_bytes([inner_bytes[4], inner_bytes[5]]);
|
||||
let message_content = &inner_bytes[6..];
|
||||
|
||||
let header = LpHeader {
|
||||
protocol_version,
|
||||
reserved: 0,
|
||||
receiver_idx: outer_header.receiver_idx,
|
||||
counter: outer_header.counter,
|
||||
};
|
||||
|
||||
let message = parse_message_from_type_and_content(msg_type, message_content)?;
|
||||
|
||||
Ok(LpPacket {
|
||||
header,
|
||||
message,
|
||||
trailer,
|
||||
})
|
||||
}
|
||||
Some(key) => {
|
||||
// AEAD decryption mode
|
||||
let nonce = build_nonce(header.counter);
|
||||
let aad = &src[..LpHeader::SIZE]; // Header as AAD
|
||||
// AAD is the outer header (12 bytes)
|
||||
let nonce = build_nonce(outer_header.counter);
|
||||
let aad = &src[..OUTER_HEADER_SIZE];
|
||||
|
||||
// Copy payload for in-place decryption
|
||||
let mut decrypted = payload_bytes.to_vec();
|
||||
// Copy inner payload for in-place decryption
|
||||
let mut decrypted = inner_bytes.to_vec();
|
||||
|
||||
// Convert trailer to Tag
|
||||
let tag = Tag::from_slice(&trailer);
|
||||
@@ -213,67 +240,85 @@ pub fn parse_lp_packet(
|
||||
.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 {
|
||||
// Decrypted format: proto(1) + reserved(3) + msg_type(2) + content
|
||||
if decrypted.len() < INNER_PREFIX_SIZE + 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,
|
||||
}
|
||||
});
|
||||
let protocol_version = decrypted[0];
|
||||
// reserved bytes [1..4] are ignored
|
||||
let msg_type = u16::from_le_bytes([decrypted[4], decrypted[5]]);
|
||||
let message_content = &decrypted[6..];
|
||||
|
||||
let header = LpHeader {
|
||||
protocol_version,
|
||||
reserved: 0,
|
||||
receiver_idx: outer_header.receiver_idx,
|
||||
counter: outer_header.counter,
|
||||
};
|
||||
|
||||
let message = parse_message_from_type_and_content(msg_type, message_content)?;
|
||||
|
||||
Ok(LpPacket {
|
||||
header,
|
||||
message,
|
||||
trailer,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
// Cleartext path: parse message from payload
|
||||
let message = parse_message_from_type_and_content(message_type_raw, message_content)?;
|
||||
|
||||
Ok(LpPacket {
|
||||
header,
|
||||
message,
|
||||
trailer,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes an LpPacket into the provided BytesMut buffer.
|
||||
///
|
||||
/// ## Unified Packet Format
|
||||
///
|
||||
/// Both cleartext and encrypted packets have the same structure:
|
||||
/// - Outer header (12B): receiver_idx(4) + counter(8) - always cleartext
|
||||
/// - Inner payload: proto(1) + reserved(3) + msg_type(2) + content - cleartext or encrypted
|
||||
/// - Trailer (16B): zeros (cleartext) or AEAD tag (encrypted)
|
||||
///
|
||||
/// # 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
|
||||
/// * `outer_key` - None for cleartext, Some for AEAD encryption
|
||||
pub fn serialize_lp_packet(
|
||||
item: &LpPacket,
|
||||
dst: &mut BytesMut,
|
||||
outer_key: Option<&OuterAeadKey>,
|
||||
) -> Result<(), LpError> {
|
||||
// Total size: outer_header(12) + inner_prefix(4) + msg_type(2) + content + trailer(16)
|
||||
let total_size = OUTER_HEADER_SIZE + INNER_PREFIX_SIZE + 2 + item.message.len() + TRAILER_LEN;
|
||||
dst.reserve(total_size);
|
||||
|
||||
// 1. Write outer header (always cleartext) - 12 bytes
|
||||
let outer_header = OuterHeader::new(item.header.receiver_idx, item.header.counter);
|
||||
outer_header.encode_into(dst);
|
||||
|
||||
match outer_key {
|
||||
None => {
|
||||
// Cleartext mode - use existing encode method
|
||||
dst.reserve(LpHeader::SIZE + 2 + item.message.len() + TRAILER_LEN);
|
||||
item.encode(dst);
|
||||
// Cleartext mode
|
||||
// 2. Write inner prefix: proto(1) + reserved(3)
|
||||
dst.put_u8(item.header.protocol_version);
|
||||
dst.put_slice(&[0, 0, 0]); // reserved
|
||||
|
||||
// 3. Write message type (2B) + content
|
||||
dst.put_slice(&(item.message.typ() as u16).to_le_bytes());
|
||||
item.message.encode_content(dst);
|
||||
|
||||
// 4. Write zeros trailer
|
||||
dst.put_slice(&[0u8; TRAILER_LEN]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Some(key) => {
|
||||
// AEAD encryption mode
|
||||
dst.reserve(LpHeader::SIZE + 2 + item.message.len() + TRAILER_LEN);
|
||||
// AAD is the outer header (first 12 bytes)
|
||||
let aad = outer_header.encode();
|
||||
|
||||
// 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
|
||||
// 2. Build plaintext: proto(1) + reserved(3) + msg_type(2) + content
|
||||
let mut plaintext = BytesMut::new();
|
||||
plaintext.put_u8(item.header.protocol_version);
|
||||
plaintext.put_slice(&[0, 0, 0]); // reserved
|
||||
plaintext.put_slice(&(item.message.typ() as u16).to_le_bytes());
|
||||
item.message.encode_content(&mut plaintext);
|
||||
|
||||
@@ -281,16 +326,15 @@ pub fn serialize_lp_packet(
|
||||
let payload_start = dst.len();
|
||||
dst.put_slice(&plaintext);
|
||||
|
||||
// 4. Build nonce and get AAD
|
||||
// 4. Build nonce from counter
|
||||
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,
|
||||
&aad,
|
||||
&mut dst[payload_start..],
|
||||
)
|
||||
.map_err(|_| LpError::Internal("AEAD encryption failed".to_string()))?;
|
||||
@@ -320,8 +364,9 @@ mod tests {
|
||||
use crate::packet::{LpHeader, LpPacket, TRAILER_LEN};
|
||||
use bytes::BytesMut;
|
||||
|
||||
// Header length: version(1) + reserved(3) + receiver_index(4) + counter(8) = 16 bytes
|
||||
const HEADER_LEN: usize = 16;
|
||||
// AIDEV-NOTE: With unified format, outer header (receiver_idx + counter) is always first
|
||||
// and is the only cleartext portion for encrypted packets
|
||||
const OUTER_HDR: usize = super::OUTER_HEADER_SIZE; // 12 bytes
|
||||
|
||||
// === Cleartext Encode/Decode Tests ===
|
||||
|
||||
@@ -483,17 +528,18 @@ mod tests {
|
||||
// This *should* parse successfully based on the logic, but the trailer is garbage.
|
||||
// Let's rethink: parse_lp_packet assumes the *entire slice* is the packet.
|
||||
// If the slice doesn't end exactly where the trailer should, it's an error.
|
||||
// In this case, total length is 58. LpHeader(16) + Type(2) + Trailer(16) = 34. Payload = 58-34=24.
|
||||
// In this case, total length is 58. OuterHdr(12) + InnerPrefix(4) + Type(2) + Trailer(16) = 34. Payload = 58-34=24.
|
||||
// Trailer starts at 16+2+24 = 42. Ends at 42+16=58. It fits exactly.
|
||||
// This test *still* doesn't test incompleteness correctly for the datagram parser.
|
||||
|
||||
// Let's test a buffer that's *too short* even for header+type+trailer+min_payload
|
||||
// Note: Buffer order doesn't matter for this test since we fail on minimum size check
|
||||
let mut buf_too_short = BytesMut::new();
|
||||
buf_too_short.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
|
||||
buf_too_short.extend_from_slice(&42u32.to_le_bytes()); // Sender index
|
||||
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.
|
||||
buf_too_short.extend_from_slice(&42u32.to_le_bytes()); // receiver_idx (outer header)
|
||||
buf_too_short.extend_from_slice(&123u64.to_le_bytes()); // counter (outer header)
|
||||
buf_too_short.extend_from_slice(&[1, 0, 0, 0]); // version + reserved (inner prefix)
|
||||
buf_too_short.extend_from_slice(&MessageType::Handshake.to_u16().to_le_bytes()); // msg type
|
||||
// No payload, no trailer. Length = 12+4+2=18. Min size = 34.
|
||||
let result_too_short = parse_lp_packet(&buf_too_short, None);
|
||||
assert!(result_too_short.is_err());
|
||||
assert!(matches!(
|
||||
@@ -873,11 +919,11 @@ mod tests {
|
||||
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]);
|
||||
// Outer header (receiver_idx + counter) should be the same - always cleartext
|
||||
assert_eq!(&cleartext[..OUTER_HDR], &encrypted[..OUTER_HDR]);
|
||||
|
||||
// Payload should differ (it's encrypted)
|
||||
let payload_start = HEADER_LEN;
|
||||
// Inner payload (proto + reserved + msg_type + content) should differ (encrypted)
|
||||
let payload_start = OUTER_HDR;
|
||||
let payload_end_cleartext = cleartext.len() - TRAILER_LEN;
|
||||
let payload_end_encrypted = encrypted.len() - TRAILER_LEN;
|
||||
|
||||
@@ -944,7 +990,8 @@ mod tests {
|
||||
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)
|
||||
// Tamper with the outer header AAD (flip a bit in counter at byte 4)
|
||||
// New format: [receiver_idx(0-3), counter(4-11)], so byte 4 is counter's LSB
|
||||
encrypted[4] ^= 0x01;
|
||||
|
||||
// Parsing should fail with AeadTagMismatch
|
||||
@@ -986,9 +1033,9 @@ mod tests {
|
||||
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;
|
||||
// The encrypted inner payloads should differ even though the message is the same
|
||||
// (because nonce is different). Inner payload starts after outer header.
|
||||
let payload_start = OUTER_HDR;
|
||||
assert_ne!(
|
||||
&encrypted1[payload_start..],
|
||||
&encrypted2[payload_start..],
|
||||
|
||||
@@ -16,7 +16,7 @@ pub mod session_manager;
|
||||
|
||||
pub use error::LpError;
|
||||
pub use message::{ClientHelloData, LpMessage};
|
||||
pub use packet::{LpPacket, BOOTSTRAP_RECEIVER_IDX};
|
||||
pub use packet::{LpPacket, OuterHeader, BOOTSTRAP_RECEIVER_IDX};
|
||||
pub use replay::{ReceivingKeyCounterValidator, ReplayError};
|
||||
pub use session::{LpSession, generate_fresh_salt};
|
||||
pub use session_manager::SessionManager;
|
||||
|
||||
@@ -130,7 +130,60 @@ impl LpPacket {
|
||||
/// the same session ID from their keys, and all subsequent packets use that ID.
|
||||
pub const BOOTSTRAP_RECEIVER_IDX: u32 = 0;
|
||||
|
||||
// VERSION [1B] || RESERVED [3B] || SENDER_INDEX [4B] || COUNTER [8B]
|
||||
/// Outer header (12 bytes) - always cleartext, used for routing.
|
||||
///
|
||||
/// This is the first 12 bytes of every LP packet, containing only the fields
|
||||
/// needed for session lookup (receiver_idx) and replay protection (counter).
|
||||
/// For encrypted packets, this is the AAD (additional authenticated data).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct OuterHeader {
|
||||
pub receiver_idx: u32,
|
||||
pub counter: u64,
|
||||
}
|
||||
|
||||
impl OuterHeader {
|
||||
pub const SIZE: usize = 12; // receiver_idx(4) + counter(8)
|
||||
|
||||
pub fn new(receiver_idx: u32, counter: u64) -> Self {
|
||||
Self {
|
||||
receiver_idx,
|
||||
counter,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(src: &[u8]) -> Result<Self, LpError> {
|
||||
if src.len() < Self::SIZE {
|
||||
return Err(LpError::InsufficientBufferSize);
|
||||
}
|
||||
Ok(Self {
|
||||
receiver_idx: u32::from_le_bytes(src[0..4].try_into().unwrap()),
|
||||
counter: u64::from_le_bytes(src[4..12].try_into().unwrap()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encode(&self) -> [u8; Self::SIZE] {
|
||||
let mut buf = [0u8; Self::SIZE];
|
||||
buf[0..4].copy_from_slice(&self.receiver_idx.to_le_bytes());
|
||||
buf[4..12].copy_from_slice(&self.counter.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
|
||||
/// Encode directly into a BytesMut buffer
|
||||
pub fn encode_into(&self, dst: &mut BytesMut) {
|
||||
dst.put_slice(&self.receiver_idx.to_le_bytes());
|
||||
dst.put_slice(&self.counter.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal LP header representation containing all logical header fields.
|
||||
///
|
||||
/// **Note**: This struct represents the LOGICAL header, not the wire format.
|
||||
/// On the wire, packets use the unified format where:
|
||||
/// - `OuterHeader` (receiver_idx + counter) always comes first (12 bytes, cleartext)
|
||||
/// - Inner content (version + reserved + payload) follows (cleartext or encrypted)
|
||||
///
|
||||
/// The `LpHeader::encode()` method outputs the old logical format for debug purposes only.
|
||||
/// Use `serialize_lp_packet()` in codec.rs for actual wire serialization.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LpHeader {
|
||||
pub protocol_version: u8,
|
||||
|
||||
@@ -6,7 +6,7 @@ use super::registration::process_registration;
|
||||
use super::LpHandlerState;
|
||||
use crate::error::GatewayError;
|
||||
use nym_lp::{
|
||||
codec::OuterAeadKey, keypair::PublicKey, message::ForwardPacketData, packet::LpHeader,
|
||||
codec::OuterAeadKey, keypair::PublicKey, message::ForwardPacketData, OuterHeader,
|
||||
LpMessage, LpPacket,
|
||||
};
|
||||
use nym_metrics::{add_histogram_obs, inc};
|
||||
@@ -775,11 +775,12 @@ impl LpConnectionHandler {
|
||||
Ok(response_buf)
|
||||
}
|
||||
|
||||
/// Receive raw packet bytes and parse header only (for routing before session lookup).
|
||||
/// Receive raw packet bytes and parse outer 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> {
|
||||
/// Returns the raw packet bytes and parsed outer header (receiver_idx + counter).
|
||||
/// 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>, OuterHeader), GatewayError> {
|
||||
use nym_lp::codec::parse_lp_header_only;
|
||||
|
||||
// Read 4-byte length prefix (u32 big-endian)
|
||||
@@ -1096,9 +1097,10 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
// Handler should receive and parse it correctly
|
||||
// Note: header is OuterHeader (receiver_idx + counter only), not LpHeader
|
||||
let (header, received) = server_task.await.unwrap().unwrap();
|
||||
assert_eq!(header.protocol_version, 1);
|
||||
assert_eq!(header.receiver_idx, 42);
|
||||
assert_eq!(header.counter, 0);
|
||||
assert_eq!(received.header().protocol_version, 1);
|
||||
assert_eq!(received.header().receiver_idx, 42);
|
||||
assert_eq!(received.header().counter, 0);
|
||||
|
||||
Reference in New Issue
Block a user