Add response validation, zeroize, and replay protection
- Validate Ack response in NestedLpSession and LP client final handshake - Replace manual Drop with derive(Zeroize, ZeroizeOnDrop) for OuterAeadKey - Add replay counter check before AEAD decryption to prevent DoS [nym-qm2q, nym-z82d, nym-9ik3, nym-62fs]
This commit is contained in:
Generated
+1
@@ -6613,6 +6613,7 @@ dependencies = [
|
||||
"tls_codec",
|
||||
"tracing",
|
||||
"utoipa",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -35,6 +35,7 @@ libcrux-traits = { git = "https://github.com/cryspen/libcrux" }
|
||||
tls_codec = { workspace = true }
|
||||
num_enum = { workspace = true }
|
||||
chacha20poly1305 = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
|
||||
@@ -12,6 +12,7 @@ use chacha20poly1305::{
|
||||
aead::{AeadInPlace, KeyInit},
|
||||
ChaCha20Poly1305, Key, Nonce, Tag,
|
||||
};
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
/// Outer AEAD key for LP packet encryption.
|
||||
///
|
||||
@@ -34,7 +35,7 @@ use chacha20poly1305::{
|
||||
/// 3. **No PSK persistence**: PSK handles are not stored/reused across sessions.
|
||||
/// Each connection performs fresh KKT+PSQ handshake.
|
||||
///
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
|
||||
pub struct OuterAeadKey {
|
||||
key: [u8; 32],
|
||||
}
|
||||
@@ -58,13 +59,6 @@ impl OuterAeadKey {
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
@@ -375,6 +375,8 @@ impl LpConnectionHandler {
|
||||
self.remote_addr, receiver_idx
|
||||
);
|
||||
|
||||
let counter = packet.header().counter();
|
||||
|
||||
// Get session and decrypt payload
|
||||
let decrypted_bytes = {
|
||||
let session_entry = self.state.session_states.get(&receiver_idx).ok_or_else(|| {
|
||||
@@ -386,10 +388,24 @@ impl LpConnectionHandler {
|
||||
|
||||
let session = &session_entry.value().state;
|
||||
|
||||
// Decrypt packet
|
||||
session.decrypt_data(packet.message()).map_err(|e| {
|
||||
// AIDEV-NOTE: Validate counter BEFORE decryption to prevent replay DoS attacks.
|
||||
// Counter is from cleartext header but authenticated by AEAD AAD, so this is safe.
|
||||
session.receiving_counter_quick_check(counter).map_err(|e| {
|
||||
inc!("lp_errors_replay_check");
|
||||
GatewayError::LpProtocolError(format!("Replay check failed: {}", e))
|
||||
})?;
|
||||
|
||||
// Decrypt packet (Noise inner layer)
|
||||
let decrypted = session.decrypt_data(packet.message()).map_err(|e| {
|
||||
GatewayError::LpProtocolError(format!("Failed to decrypt packet: {}", e))
|
||||
})?
|
||||
})?;
|
||||
|
||||
// Mark counter as received after successful decryption
|
||||
session.receiving_counter_mark(counter).map_err(|e| {
|
||||
GatewayError::LpProtocolError(format!("Failed to mark counter: {}", e))
|
||||
})?;
|
||||
|
||||
decrypted
|
||||
};
|
||||
|
||||
// Try to deserialize as LpRegistrationRequest first (most common case after handshake)
|
||||
|
||||
@@ -292,14 +292,28 @@ impl LpRegistrationClient {
|
||||
.ok()
|
||||
.and_then(|s| s.outer_aead_key());
|
||||
tracing::trace!("Sending final handshake packet");
|
||||
// Final packet - we don't need the response
|
||||
let _ = Self::connect_send_receive(
|
||||
let ack_response = Self::connect_send_receive(
|
||||
self.gateway_lp_address,
|
||||
&final_packet,
|
||||
outer_key.as_ref(),
|
||||
&self.config,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Validate Ack response
|
||||
match ack_response.message() {
|
||||
nym_lp::LpMessage::Ack => {
|
||||
tracing::debug!(
|
||||
"Received Ack for final handshake packet"
|
||||
);
|
||||
}
|
||||
other => {
|
||||
return Err(LpClientError::Transport(format!(
|
||||
"Expected Ack for final handshake packet, got: {:?}",
|
||||
other
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
tracing::info!("LP handshake completed after sending final packet");
|
||||
break;
|
||||
|
||||
@@ -153,7 +153,7 @@ impl NestedLpSession {
|
||||
|
||||
// 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
|
||||
let response_bytes = outer_client
|
||||
.send_forward_packet(
|
||||
self.exit_identity,
|
||||
self.exit_address.clone(),
|
||||
@@ -161,7 +161,25 @@ impl NestedLpSession {
|
||||
)
|
||||
.await?;
|
||||
|
||||
tracing::debug!("Sent ClientHello to exit gateway via entry");
|
||||
// Parse and validate Ack response (cleartext, no outer key before PSK derivation)
|
||||
let ack_response = Self::parse_packet(&response_bytes, None)?;
|
||||
match ack_response.message() {
|
||||
LpMessage::Ack => {
|
||||
tracing::debug!("Received Ack for ClientHello from exit gateway");
|
||||
}
|
||||
LpMessage::Collision => {
|
||||
return Err(LpClientError::Transport(format!(
|
||||
"Exit gateway returned Collision - receiver_index {} already in use",
|
||||
receiver_index
|
||||
)));
|
||||
}
|
||||
other => {
|
||||
return Err(LpClientError::Transport(format!(
|
||||
"Expected Ack for ClientHello from exit gateway, got: {:?}",
|
||||
other
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Create state machine for exit gateway handshake
|
||||
let mut state_machine = LpStateMachine::new(
|
||||
|
||||
Reference in New Issue
Block a user