From 9eb8cc2de80ac2174cc39f9b742488e87f28cd53 Mon Sep 17 00:00:00 2001 From: durch Date: Tue, 16 Dec 2025 21:12:32 +0100 Subject: [PATCH] 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 --- common/nym-lp/src/codec.rs | 60 ++++++++++--------- common/nym-lp/src/config.rs | 2 +- common/nym-lp/src/error.rs | 2 +- common/nym-lp/src/message.rs | 8 +-- common/nym-lp/src/psk.rs | 4 +- common/nym-lp/src/state_machine.rs | 2 +- gateway/src/node/lp_listener/handler.rs | 12 +++- gateway/src/node/lp_listener/mod.rs | 2 +- .../src/lp_client/nested_session.rs | 5 +- 9 files changed, 56 insertions(+), 41 deletions(-) diff --git a/common/nym-lp/src/codec.rs b/common/nym-lp/src/codec.rs index 0d549951d1..c1e52b17f7 100644 --- a/common/nym-lp/src/codec.rs +++ b/common/nym-lp/src/codec.rs @@ -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 { - 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 { /// /// 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 { // 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) -> Result, ) -> 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]); diff --git a/common/nym-lp/src/config.rs b/common/nym-lp/src/config.rs index 824d56e7ac..4b22ad331d 100644 --- a/common/nym-lp/src/config.rs +++ b/common/nym-lp/src/config.rs @@ -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. diff --git a/common/nym-lp/src/error.rs b/common/nym-lp/src/error.rs index 2f5d52f836..3e6592313d 100644 --- a/common/nym-lp/src/error.rs +++ b/common/nym-lp/src/error.rs @@ -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), diff --git a/common/nym-lp/src/message.rs b/common/nym-lp/src/message.rs index 6c0c7b1427..aac66e0910 100644 --- a/common/nym-lp/src/message.rs +++ b/common/nym-lp/src/message.rs @@ -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 { + pub(crate) fn from_u32(value: u32) -> Option { MessageType::try_from(value).ok() } - pub fn to_u16(&self) -> u16 { - u16::from(*self) + pub fn to_u32(&self) -> u32 { + u32::from(*self) } } diff --git a/common/nym-lp/src/psk.rs b/common/nym-lp/src/psk.rs index 502701a936..ea48db9415 100644 --- a/common/nym-lp/src/psk.rs +++ b/common/nym-lp/src/psk.rs @@ -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) diff --git a/common/nym-lp/src/state_machine.rs b/common/nym-lp/src/state_machine.rs index db46168369..ddd63e8d13 100644 --- a/common/nym-lp/src/state_machine.rs +++ b/common/nym-lp/src/state_machine.rs @@ -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) diff --git a/gateway/src/node/lp_listener/handler.rs b/gateway/src/node/lp_listener/handler.rs index 9052183b17..2cc8a1f162 100644 --- a/gateway/src/node/lp_listener/handler.rs +++ b/gateway/src/node/lp_listener/handler.rs @@ -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, diff --git a/gateway/src/node/lp_listener/mod.rs b/gateway/src/node/lp_listener/mod.rs index c5545c51e9..7eefd1b9f3 100644 --- a/gateway/src/node/lp_listener/mod.rs +++ b/gateway/src/node/lp_listener/mod.rs @@ -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. diff --git a/nym-registration-client/src/lp_client/nested_session.rs b/nym-registration-client/src/lp_client/nested_session.rs index 667b3d36db..e9fcbebab2 100644 --- a/nym-registration-client/src/lp_client/nested_session.rs +++ b/nym-registration-client/src/lp_client/nested_session.rs @@ -636,7 +636,10 @@ impl NestedLpSession { /// /// Returns `None` during early handshake before PSK derivation. fn get_recv_key(state_machine: &LpStateMachine) -> Option { - 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.