Change MessageType from u16 to u32

Breaking wire protocol change: MessageType field increased from 2 bytes
to 4 bytes in LP packets. This future-proofs the message type space and
aligns with other u32 fields.

Changes:
- message.rs: #[repr(u32)], from_u32(), to_u32()
- error.rs: InvalidMessageType(u32)
- codec.rs: All serialization/deserialization updated to 4-byte msg_type
  - Cleartext parsing: inner_bytes[4..8], content at [8..]
  - AEAD parsing: decrypted[4..8], content at [8..]
  - Serialization: 4 bytes for message type
This commit is contained in:
durch
2025-12-16 21:12:32 +01:00
committed by Jędrzej Stuczyński
parent 7ebacde1fc
commit 9eb8cc2de8
9 changed files with 56 additions and 41 deletions
+33 -27
View File
@@ -89,10 +89,10 @@ fn build_nonce(counter: u64) -> [u8; 12] {
/// 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,
msg_type_raw: u32,
content: &[u8],
) -> Result<LpMessage, LpError> {
let message_type = MessageType::from_u16(msg_type_raw)
let message_type = MessageType::from_u32(msg_type_raw)
.ok_or_else(|| LpError::invalid_message_type(msg_type_raw))?;
match message_type {
@@ -195,7 +195,7 @@ pub fn parse_lp_header_only(src: &[u8]) -> Result<OuterHeader, LpError> {
///
/// 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
/// - Inner payload: proto(1) + reserved(3) + msg_type(4) + content - cleartext or encrypted
/// - Trailer (16B): zeros (cleartext) or AEAD tag (encrypted)
///
/// # Arguments
@@ -228,15 +228,20 @@ pub fn parse_lp_packet(src: &[u8], outer_key: Option<&OuterAeadKey>) -> Result<L
match outer_key {
None => {
// Cleartext mode - parse inner directly
// Inner format: proto(1) + reserved(3) + msg_type(2) + content
if inner_bytes.len() < INNER_PREFIX_SIZE + 2 {
// Inner format: proto(1) + reserved(3) + msg_type(4) + content
if inner_bytes.len() < INNER_PREFIX_SIZE + 4 {
return Err(LpError::InsufficientBufferSize);
}
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 msg_type = u32::from_le_bytes([
inner_bytes[4],
inner_bytes[5],
inner_bytes[6],
inner_bytes[7],
]);
let message_content = &inner_bytes[8..];
let header = LpHeader {
protocol_version,
@@ -271,15 +276,16 @@ pub fn parse_lp_packet(src: &[u8], outer_key: Option<&OuterAeadKey>) -> Result<L
.decrypt_in_place_detached(Nonce::from_slice(&nonce), aad, &mut decrypted, tag)
.map_err(|_| LpError::AeadTagMismatch)?;
// Decrypted format: proto(1) + reserved(3) + msg_type(2) + content
if decrypted.len() < INNER_PREFIX_SIZE + 2 {
// Decrypted format: proto(1) + reserved(3) + msg_type(4) + content
if decrypted.len() < INNER_PREFIX_SIZE + 4 {
return Err(LpError::InsufficientBufferSize);
}
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 msg_type =
u32::from_le_bytes([decrypted[4], decrypted[5], decrypted[6], decrypted[7]]);
let message_content = &decrypted[8..];
let header = LpHeader {
protocol_version,
@@ -305,7 +311,7 @@ pub fn parse_lp_packet(src: &[u8], outer_key: Option<&OuterAeadKey>) -> Result<L
///
/// 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
/// - Inner payload: proto(1) + reserved(3) + msg_type(4) + content - cleartext or encrypted
/// - Trailer (16B): zeros (cleartext) or AEAD tag (encrypted)
///
/// # Arguments
@@ -317,8 +323,8 @@ pub fn serialize_lp_packet(
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;
// Total size: outer_header(12) + inner_prefix(4) + msg_type(4) + content + trailer(16)
let total_size = OUTER_HEADER_SIZE + INNER_PREFIX_SIZE + 4 + item.message.len() + TRAILER_LEN;
dst.reserve(total_size);
// 1. Write outer header (always cleartext) - 12 bytes
@@ -332,8 +338,8 @@ pub fn serialize_lp_packet(
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());
// 3. Write message type (4B) + content
dst.put_slice(&(item.message.typ() as u32).to_le_bytes());
item.message.encode_content(dst);
// 4. Write zeros trailer
@@ -346,11 +352,11 @@ pub fn serialize_lp_packet(
// AAD is the outer header (first 12 bytes)
let aad = outer_header.encode();
// 2. Build plaintext: proto(1) + reserved(3) + msg_type(2) + content
// 2. Build plaintext: proto(1) + reserved(3) + msg_type(4) + 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());
plaintext.put_slice(&(item.message.typ() as u32).to_le_bytes());
item.message.encode_content(&mut plaintext);
// 3. Copy plaintext to dst for in-place encryption
@@ -380,7 +386,7 @@ pub fn serialize_lp_packet(
// Add a new error variant for invalid message types (Moved from previous impl LpError block)
impl LpError {
pub fn invalid_message_type(message_type: u16) -> Self {
pub fn invalid_message_type(message_type: u32) -> Self {
LpError::InvalidMessageType(message_type)
}
}
@@ -549,7 +555,7 @@ mod tests {
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index
buf.extend_from_slice(&123u64.to_le_bytes()); // Counter
buf.extend_from_slice(&MessageType::Handshake.to_u16().to_le_bytes()); // Handshake type
buf.extend_from_slice(&MessageType::Handshake.to_u32().to_le_bytes()); // Handshake type
buf.extend_from_slice(&[42; 40]); // 40 bytes of payload data
// Buffer length = 16 + 2 + 40 = 58. Min size = 16 + 2 + 16 = 34.
@@ -569,7 +575,7 @@ mod tests {
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
buf_too_short.extend_from_slice(&MessageType::Handshake.to_u32().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());
@@ -583,7 +589,7 @@ mod tests {
buf_partial_trailer.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
buf_partial_trailer.extend_from_slice(&42u32.to_le_bytes()); // Sender index
buf_partial_trailer.extend_from_slice(&123u64.to_le_bytes()); // Counter
buf_partial_trailer.extend_from_slice(&MessageType::Handshake.to_u16().to_le_bytes()); // Handshake type
buf_partial_trailer.extend_from_slice(&MessageType::Handshake.to_u32().to_le_bytes()); // Handshake type
let payload = vec![42u8; 20]; // Assume 20 byte payload
buf_partial_trailer.extend_from_slice(&payload);
buf_partial_trailer.extend_from_slice(&[0; TRAILER_LEN - 1]); // Missing last byte of trailer
@@ -605,7 +611,7 @@ mod tests {
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::Busy.to_u16().to_le_bytes()); // Type
buf_too_short.extend_from_slice(&MessageType::Busy.to_u32().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, None);
@@ -649,7 +655,7 @@ mod tests {
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index
buf.extend_from_slice(&123u64.to_le_bytes()); // Counter
buf.extend_from_slice(&MessageType::Busy.to_u16().to_le_bytes()); // Busy type
buf.extend_from_slice(&MessageType::Busy.to_u32().to_le_bytes()); // Busy type
buf.extend_from_slice(&[42; 1]); // <<< Invalid 1-byte payload for Busy
buf.extend_from_slice(&[0; TRAILER_LEN]); // Trailer
@@ -778,7 +784,7 @@ mod tests {
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index
buf.extend_from_slice(&123u64.to_le_bytes()); // Counter
buf.extend_from_slice(&MessageType::ClientHello.to_u16().to_le_bytes()); // ClientHello type
buf.extend_from_slice(&MessageType::ClientHello.to_u32().to_le_bytes()); // ClientHello type
// Add malformed bincode data (random bytes that won't deserialize to ClientHelloData)
buf.extend_from_slice(&[0xFF; 50]); // Invalid bincode data
@@ -801,7 +807,7 @@ mod tests {
buf.extend_from_slice(&[1, 0, 0, 0]); // Version + reserved
buf.extend_from_slice(&42u32.to_le_bytes()); // Sender index
buf.extend_from_slice(&123u64.to_le_bytes()); // Counter
buf.extend_from_slice(&MessageType::ClientHello.to_u16().to_le_bytes()); // ClientHello type
buf.extend_from_slice(&MessageType::ClientHello.to_u32().to_le_bytes()); // ClientHello type
// Add incomplete bincode data (only partial ClientHelloData)
buf.extend_from_slice(&[0; 20]); // Too few bytes for full ClientHelloData
@@ -1296,7 +1302,7 @@ mod tests {
buf.extend_from_slice(&42u32.to_le_bytes()); // receiver_idx
buf.extend_from_slice(&123u64.to_le_bytes()); // counter
buf.extend_from_slice(&[1, 0, 0, 0]); // version + reserved
buf.extend_from_slice(&MessageType::SubsessionRequest.to_u16().to_le_bytes());
buf.extend_from_slice(&MessageType::SubsessionRequest.to_u32().to_le_bytes());
buf.extend_from_slice(&[0xFF]); // Invalid payload for SubsessionRequest
buf.extend_from_slice(&[0; TRAILER_LEN]);
+1 -1
View File
@@ -3,7 +3,7 @@
//! Configuration for LP protocol.
//!
//! AIDEV-NOTE: LP security stack = KKT (key fetch) → PSQ (PQ PSK) → Noise (transport).
//! LP security stack = KKT (key fetch) → PSQ (PQ PSK) → Noise (transport).
//! KEM algorithm selection affects only PSQ layer. Noise always uses X25519 DH.
//! Migration to PQ KEMs (MlKem768, XWing) requires only config change.
+1 -1
View File
@@ -26,7 +26,7 @@ pub enum LpError {
InvalidPacketFormat(String),
#[error("Invalid message type: {0}")]
InvalidMessageType(u16),
InvalidMessageType(u32),
#[error("Payload too large: {0}")]
PayloadTooLarge(usize),
+4 -4
View File
@@ -68,7 +68,7 @@ impl ClientHelloData {
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)]
#[repr(u16)]
#[repr(u32)]
pub enum MessageType {
Busy = 0x0000,
Handshake = 0x0001,
@@ -94,12 +94,12 @@ pub enum MessageType {
}
impl MessageType {
pub(crate) fn from_u16(value: u16) -> Option<Self> {
pub(crate) fn from_u32(value: u32) -> Option<Self> {
MessageType::try_from(value).ok()
}
pub fn to_u16(&self) -> u16 {
u16::from(*self)
pub fn to_u32(&self) -> u32 {
u32::from(*self)
}
}
+2 -2
View File
@@ -6,7 +6,7 @@
//! This module implements identity-bound PSK derivation where both client and gateway
//! derive the same PSK from their LP keypairs.
//!
//! AIDEV-NOTE: PSQ is embedded in Noise (not separate protocol) because:
//! PSQ is embedded in Noise (not separate protocol) because:
//! 1. Single round-trip: PSQ ciphertext piggybacks on Noise handshake messages
//! 2. PSK binding: Noise XKpsk3 pattern authenticates both ECDH and PSQ-derived PSK
//! 3. Simpler state machine: No separate PSQ negotiation phase needed
@@ -146,7 +146,7 @@ pub fn derive_psk_with_psq_initiator(
let ecdh_secret = local_x25519_private.diffie_hellman(remote_x25519_public);
// Step 2: PSQ encapsulation for post-quantum security
// AIDEV-NOTE: KEM algorithm migration path:
// KEM algorithm migration path:
// - X25519: Current default for testing/compatibility (no HNDL resistance)
// - MlKem768: Future production default (NIST PQ Level 3, HNDL resistant)
// - XWing: Maximum security option (hybrid X25519 + ML-KEM)
+1 -1
View File
@@ -3,7 +3,7 @@
//! Lewes Protocol State Machine for managing connection lifecycle.
//!
//! AIDEV-NOTE: LP protocol flow (KKT → PSQ → Noise):
//! LP protocol flow (KKT → PSQ → Noise):
//! 1. KKTExchange: Client requests gateway's KEM public key (signed for MITM protection)
//! 2. Handshaking: Noise XKpsk3 with PSQ-derived PSK embedded in handshake messages
//! - PSQ ciphertext piggybacked on ClientHello (no extra round-trip)
+9 -3
View File
@@ -453,7 +453,12 @@ impl LpConnectionHandler {
// Get outer key before releasing borrow
let outer_key = state_machine
.session()
.map_err(|e| GatewayError::LpProtocolError(format!("Session unavailable after processing: {}", e)))?
.map_err(|e| {
GatewayError::LpProtocolError(format!(
"Session unavailable after processing: {}",
e
))
})?
.outer_aead_key();
drop(state_entry);
@@ -1130,8 +1135,9 @@ mod tests {
timestamp_tolerance_secs: 30,
..Default::default()
};
let forward_semaphore =
Arc::new(tokio::sync::Semaphore::new(lp_config.max_concurrent_forwards));
let forward_semaphore = Arc::new(tokio::sync::Semaphore::new(
lp_config.max_concurrent_forwards,
));
LpHandlerState {
lp_config,
+1 -1
View File
@@ -361,7 +361,7 @@ pub struct LpHandlerState {
/// Prevents file descriptor exhaustion when forwarding LP packets during
/// telescope setup. When at capacity, forward requests return an error
/// so clients can choose a different gateway.
// AIDEV-NOTE: Connection limiting (not pooling) chosen for forward requests.
// Connection limiting (not pooling) chosen for forward requests.
//
// Why not connection pooling?
// 1. Forwarding is one-time per telescope setup (handshake only), not ongoing traffic.
@@ -636,7 +636,10 @@ impl NestedLpSession {
///
/// Returns `None` during early handshake before PSK derivation.
fn get_recv_key(state_machine: &LpStateMachine) -> Option<OuterAeadKey> {
state_machine.session().ok().and_then(|s| s.outer_aead_key())
state_machine
.session()
.ok()
.and_then(|s| s.outer_aead_key())
}
/// Serializes an LP packet to bytes.