Skip to content

Commit cf70faf

Browse files
committed
Pad network messages to fixed size when supported
When both parties signal support for `option_message_padding`, we pad any sent messages to a fixed size to improve privacy in the face of an adversary monitoring network traffic. To this end we utilize an optional TLV-stream extension with an odd field number of `u64::max_value()` that simply will be discarded by the counterparty. The padding threshold is chosen to fit even the largest standard Lightning messages (UpdateAddHtlc) whith some leeway to guarantee package size uniformity even when some of the optional fields are set. Note that even without padding we surpassed the standard Ethernet MTU of 1500 bytes for `UpdateAddHtlc` messages, so fitting the packets into exactly 1500 bytes is a futile endeavor. Furthermore note that any messages above that threshold size will still stand out in monitored network traffic. Lastly, we opt to *not* apply padding for any custom messages, as they might not be set up to handle the optional TLV extension.
1 parent 9ecf891 commit cf70faf

File tree

3 files changed

+92
-10
lines changed

3 files changed

+92
-10
lines changed

fuzz/src/peer_crypt.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ pub fn do_test(data: &[u8]) {
8181
if get_slice!(1)[0] == 0 {
8282
crypter.encrypt_buffer(MessageBuf::from_encoded(&get_slice!(slice_to_be16(
8383
get_slice!(2)
84-
))));
84+
))), false);
8585
} else {
8686
let len = match crypter.decrypt_length_header(get_slice!(16 + 2)) {
8787
Ok(len) => len,

lightning/src/ln/peer_channel_encryptor.rs

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
use crate::prelude::*;
1111

12+
use crate::io::Write;
1213
use crate::ln::msgs;
1314
use crate::ln::msgs::LightningError;
1415
use crate::ln::wire;
@@ -26,7 +27,7 @@ use bitcoin::secp256k1::{PublicKey, SecretKey};
2627

2728
use crate::crypto::chacha20poly1305rfc::ChaCha20Poly1305RFC;
2829
use crate::crypto::utils::hkdf_extract_expand_twice;
29-
use crate::util::ser::VecWriter;
30+
use crate::util::ser::{BigSize, VecWriter, Writeable};
3031

3132
use core::ops::Deref;
3233

@@ -555,23 +556,85 @@ impl PeerChannelEncryptor {
555556
}
556557
}
557558

559+
fn maybe_add_message_padding(&self, buffer: &mut Vec<u8>) {
560+
// In the base case, a serialized UpdateAddHTLC message is 1450 bytes: 32 (channel_id) + 8
561+
// (htlc_id) + 8 (amount_msat) + 32 (payment_hash) + 4 (cltv_expiry) + 1366
562+
// (onion_routing_packet). When including the additional 2 (encrypted message length) + 16
563+
// (encrypted message length MAC) + 2 (type) bytes, this has us at 1470 bytes
564+
// pre-encryption. As the encryption step adds 16 more bytes for the MAC of the encrypted
565+
// message itself, resulting in 1486 bytes TCP payload.
566+
//
567+
// As this base case however doesn't take into account any potential optional fields that
568+
// might be set on UpdateAddHTLC (such as the `path_key` for route blinding or other TLVs),
569+
// we opt to add another 50 bytes of leeway to our padding threshold size.
570+
//
571+
// Note that anything above this threshold won't get padded and will stand out in monitored
572+
// network traffic.
573+
const PADDING_THRESHOLD_BYTES: usize = 1470 + 50;
574+
575+
let orig_buffer_len = buffer.len();
576+
let padding_len =
577+
PADDING_THRESHOLD_BYTES.checked_sub(orig_buffer_len).map_or(0, |expected_len| {
578+
// As the TLV's length BigSize grows as we add more padding bytes, we might end up with
579+
// slightly larger messages than expected. To that end, we here account for this and
580+
// reduce the number of padding bytes by any serialized length of the BigSize beyond 1.
581+
//
582+
// TODO: This method risks that by subtracting the overhead we fall again just below
583+
// the `BigSize` steps which could leak the original padding len (and hence the
584+
// original message size). We should look into making this even more exact.
585+
let big_size_overhead =
586+
BigSize(expected_len as u64).serialized_length().saturating_sub(1);
587+
expected_len.saturating_sub(big_size_overhead)
588+
});
589+
590+
// We always add type and length headers so unpadded messages just at
591+
// PADDING_THRESHOLD_BYTES don't stand out.
592+
BigSize(u64::max_value())
593+
.write(buffer)
594+
.expect("In-memory messages must never fail to serialize");
595+
BigSize(padding_len as u64)
596+
.write(buffer)
597+
.expect("In-memory messages must never fail to serialize");
598+
let mut bytes_written: usize = 0;
599+
while bytes_written < padding_len {
600+
// Write padding in 32-byte chunks if possible.
601+
const PAD_BYTES_LEN: usize = 32;
602+
let pad_bytes = [42u8; PAD_BYTES_LEN];
603+
let bytes_to_write = (padding_len - bytes_written).min(PAD_BYTES_LEN);
604+
buffer
605+
.write_all(&pad_bytes[..bytes_to_write])
606+
.expect("In-memory messages must never fail to serialize");
607+
bytes_written += bytes_to_write;
608+
}
609+
610+
#[cfg(debug_assertions)]
611+
if orig_buffer_len < PADDING_THRESHOLD_BYTES {
612+
debug_assert_eq!(buffer.len(), PADDING_THRESHOLD_BYTES + 9 + 1);
613+
}
614+
}
615+
558616
/// Encrypts the given pre-serialized message, returning the encrypted version.
559617
/// panics if msg.len() > 65535 or Noise handshake has not finished.
560-
pub fn encrypt_buffer(&mut self, mut msg: MessageBuf) -> Vec<u8> {
618+
pub fn encrypt_buffer(&mut self, mut msg: MessageBuf, should_pad: bool) -> Vec<u8> {
619+
if should_pad {
620+
self.maybe_add_message_padding(&mut msg.0);
621+
}
561622
self.encrypt_message_with_header_0s(&mut msg.0);
562623
msg.0
563624
}
564625

565626
/// Encrypts the given message, returning the encrypted version.
566627
/// panics if the length of `message`, once encoded, is greater than 65535 or if the Noise
567628
/// handshake has not finished.
568-
pub fn encrypt_message<M: wire::Type>(&mut self, message: &M) -> Vec<u8> {
629+
pub fn encrypt_message<M: wire::Type>(&mut self, message: &M, should_pad: bool) -> Vec<u8> {
569630
// Allocate a buffer with 2KB, fitting most common messages. Reserve the first 16+2 bytes
570631
// for the 2-byte message type prefix and its MAC.
571632
let mut res = VecWriter(Vec::with_capacity(MSG_BUF_ALLOC_SIZE));
572633
res.0.resize(16 + 2, 0);
573634
wire::write(message, &mut res).expect("In-memory messages must never fail to serialize");
574-
635+
if should_pad {
636+
self.maybe_add_message_padding(&mut res.0);
637+
}
575638
self.encrypt_message_with_header_0s(&mut res.0);
576639
res.0
577640
}
@@ -1015,7 +1078,7 @@ mod tests {
10151078

10161079
for i in 0..1005 {
10171080
let msg = [0x68, 0x65, 0x6c, 0x6c, 0x6f];
1018-
let mut res = outbound_peer.encrypt_buffer(MessageBuf::from_encoded(&msg));
1081+
let mut res = outbound_peer.encrypt_buffer(MessageBuf::from_encoded(&msg), false);
10191082
assert_eq!(res.len(), 5 + 2 * 16 + 2);
10201083

10211084
let len_header = res[0..2 + 16].to_vec();
@@ -1060,7 +1123,7 @@ mod tests {
10601123
fn max_message_len_encryption() {
10611124
let mut outbound_peer = get_outbound_peer_for_initiator_test_vectors();
10621125
let msg = [4u8; LN_MAX_MSG_LEN + 1];
1063-
outbound_peer.encrypt_buffer(MessageBuf::from_encoded(&msg));
1126+
outbound_peer.encrypt_buffer(MessageBuf::from_encoded(&msg), false);
10641127
}
10651128

10661129
#[test]

lightning/src/ln/peer_handler.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1578,9 +1578,14 @@ where
15781578
}
15791579
if peer.should_buffer_gossip_broadcast() {
15801580
if let Some(msg) = peer.gossip_broadcast_buffer.pop_front() {
1581+
let should_pad = peer.their_node_id.is_some_and(|(peer_id, _)| {
1582+
let our_features = self.init_features(peer_id);
1583+
our_features.supports_message_padding()
1584+
});
1585+
15811586
peer.msgs_sent_since_pong += 1;
15821587
peer.pending_outbound_buffer
1583-
.push_back(peer.channel_encryptor.encrypt_buffer(msg));
1588+
.push_back(peer.channel_encryptor.encrypt_buffer(msg, should_pad));
15841589
}
15851590
}
15861591
if peer.should_buffer_gossip_backfill() {
@@ -1739,8 +1744,18 @@ where
17391744
} else {
17401745
debug_assert!(false, "node_id should be set by the time we send a message");
17411746
}
1747+
1748+
let message_padding_supported = their_node_id.is_some_and(|peer_id| {
1749+
let our_features = self.init_features(peer_id);
1750+
our_features.supports_message_padding()
1751+
});
1752+
1753+
// Opt out of message padding for custom messages as we're not certain the application
1754+
// layer protocol can handle TLV exteensions.
1755+
let should_pad = !is_custom_msg(message.type_id()) && message_padding_supported;
17421756
peer.msgs_sent_since_pong += 1;
1743-
peer.pending_outbound_buffer.push_back(peer.channel_encryptor.encrypt_message(message));
1757+
peer.pending_outbound_buffer
1758+
.push_back(peer.channel_encryptor.encrypt_message(message, should_pad));
17441759
}
17451760

17461761
fn do_read_event(
@@ -3697,6 +3712,10 @@ fn is_gossip_msg(type_id: u16) -> bool {
36973712
}
36983713
}
36993714

3715+
fn is_custom_msg(type_id: u16) -> bool {
3716+
type_id >= 32768
3717+
}
3718+
37003719
#[cfg(test)]
37013720
mod tests {
37023721
use super::*;
@@ -4261,7 +4280,7 @@ mod tests {
42614280
peers[0].read_event(&mut fd_dup, &act_three).unwrap();
42624281

42634282
let not_init_msg = msgs::Ping { ponglen: 4, byteslen: 0 };
4264-
let msg_bytes = dup_encryptor.encrypt_message(&not_init_msg);
4283+
let msg_bytes = dup_encryptor.encrypt_message(&not_init_msg, false);
42654284
assert!(peers[0].read_event(&mut fd_dup, &msg_bytes).is_err());
42664285
}
42674286

0 commit comments

Comments
 (0)