Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion fuzz/src/peer_crypt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ pub fn do_test(data: &[u8]) {
if get_slice!(1)[0] == 0 {
crypter.encrypt_buffer(MessageBuf::from_encoded(&get_slice!(slice_to_be16(
get_slice!(2)
))));
))), false);
} else {
let len = match crypter.decrypt_length_header(get_slice!(16 + 2)) {
Ok(len) => len,
Expand Down
15 changes: 14 additions & 1 deletion lightning-types/src/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@
//! (see [BOLT PR #1160](https://github.com/lightning/bolts/pull/1160) for more information).
//! - `HtlcHold` - requires/supports holding HTLCs and forwarding on receipt of an onion message
//! (see [BOLT-2](https://github.com/lightning/bolts/pull/989/files) for more information).
//! - `MessagePaddingPrototype` - requires/supports padding of network messages for improved privacy
//! (see [BOLT-1](https://github.com/lightning/bolts/pull/1304) for more information).
//!
//! LDK knows about the following features, but does not support them:
//! - `AnchorsNonzeroFeeHtlcTx` - the initial version of anchor outputs, which was later found to be
Expand Down Expand Up @@ -174,7 +176,7 @@ mod sealed {
// Byte 18
,
// Byte 19
HtlcHold | SplicePrototype,
HtlcHold | SplicePrototype | MessagePaddingPrototype,
]
);
define_context!(
Expand Down Expand Up @@ -732,6 +734,17 @@ mod sealed {
supports_splicing,
requires_splicing
);
define_feature!(
157, // BOLTs PR uses 68/69
MessagePaddingPrototype,
[InitContext],
"Feature flags for network message padding.",
set_message_padding_optional,
set_message_padding_required,
clear_message_padding,
supports_message_padding,
requires_message_padding
);
define_feature!(
259,
DnsResolver,
Expand Down
8 changes: 8 additions & 0 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15876,6 +15876,14 @@ pub fn provided_init_features(config: &UserConfig) -> InitFeatures {
features.set_htlc_hold_optional();
}

if config.enable_htlc_hold {
features.set_htlc_hold_optional();
}

if config.enable_message_padding {
features.set_message_padding_optional();
}

features
}

Expand Down
93 changes: 85 additions & 8 deletions lightning/src/ln/peer_channel_encryptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@

use crate::prelude::*;

use crate::io::Write;
use crate::ln::msgs;
use crate::ln::msgs::LightningError;
use crate::ln::wire;
use crate::sign::{NodeSigner, Recipient};
use crate::sign::{EntropySource, NodeSigner, RandomBytes, Recipient};

use bitcoin::hashes::sha256::Hash as Sha256;
use bitcoin::hashes::{Hash, HashEngine};
Expand All @@ -26,7 +27,7 @@ use bitcoin::secp256k1::{PublicKey, SecretKey};

use crate::crypto::chacha20poly1305rfc::ChaCha20Poly1305RFC;
use crate::crypto::utils::hkdf_extract_expand_twice;
use crate::util::ser::VecWriter;
use crate::util::ser::{BigSize, VecWriter, Writeable};

use core::ops::Deref;

Expand Down Expand Up @@ -105,8 +106,8 @@ enum NoiseState {

pub struct PeerChannelEncryptor {
their_node_id: Option<PublicKey>, // filled in for outbound, or inbound after noise_state is Finished

noise_state: NoiseState,
padding_entropy_source: RandomBytes,
}

impl PeerChannelEncryptor {
Expand All @@ -118,13 +119,20 @@ impl PeerChannelEncryptor {
sha.input(&their_node_id.serialize()[..]);
let h = Sha256::from_engine(sha).to_byte_array();

let mut padding_seed_engine = Sha256::engine();
padding_seed_engine.input(b"LDK MESSAGE PADDING");
padding_seed_engine.input(&h);
let padding_seed = Sha256::from_engine(padding_seed_engine).to_byte_array();
let padding_entropy_source = RandomBytes::new(padding_seed);

PeerChannelEncryptor {
their_node_id: Some(their_node_id),
noise_state: NoiseState::InProgress {
state: NoiseStep::PreActOne,
directional_state: DirectionalNoiseState::Outbound { ie: ephemeral_key },
bidirectional_state: BidirectionalNoiseState { h, ck: NOISE_CK },
},
padding_entropy_source,
}
}

Expand All @@ -138,6 +146,12 @@ impl PeerChannelEncryptor {
sha.input(&our_node_id.serialize()[..]);
let h = Sha256::from_engine(sha).to_byte_array();

let mut padding_seed_engine = Sha256::engine();
padding_seed_engine.input(b"LDK MESSAGE PADDING");
padding_seed_engine.input(&h);
let padding_seed = Sha256::from_engine(padding_seed_engine).to_byte_array();
let padding_entropy_source = RandomBytes::new(padding_seed);

PeerChannelEncryptor {
their_node_id: None,
noise_state: NoiseState::InProgress {
Expand All @@ -149,6 +163,7 @@ impl PeerChannelEncryptor {
},
bidirectional_state: BidirectionalNoiseState { h, ck: NOISE_CK },
},
padding_entropy_source,
}
}

Expand Down Expand Up @@ -555,23 +570,85 @@ impl PeerChannelEncryptor {
}
}

fn maybe_add_message_padding(&self, buffer: &mut Vec<u8>) {
// In the base case, a serialized UpdateAddHTLC message is 1450 bytes: 32 (channel_id) + 8
// (htlc_id) + 8 (amount_msat) + 32 (payment_hash) + 4 (cltv_expiry) + 1366
// (onion_routing_packet). When including the additional 2 (encrypted message length) + 16
// (encrypted message length MAC) + 2 (type) bytes, this has us at 1470 bytes
// pre-encryption. As the encryption step adds 16 more bytes for the MAC of the encrypted
// message itself, resulting in 1486 bytes TCP payload.
//
// As this base case however doesn't take into account any potential optional fields that
// might be set on UpdateAddHTLC (such as the `path_key` for route blinding or other TLVs),
// we opt to add another 50 bytes of leeway to our padding threshold size.
//
// Note that anything above this threshold won't get padded and will stand out in monitored
// network traffic.
const PADDING_THRESHOLD_BYTES: usize = 1470 + 50;

let orig_buffer_len = buffer.len();
let padding_len =
PADDING_THRESHOLD_BYTES.checked_sub(orig_buffer_len).map_or(0, |expected_len| {
// As the TLV's length BigSize grows as we add more padding bytes, we might end up with
// slightly larger messages than expected. To that end, we here account for this and
// reduce the number of padding bytes by any serialized length of the BigSize beyond 1.
//
// TODO: This method risks that by subtracting the overhead we fall again just below
// the `BigSize` steps which could leak the original padding len (and hence the
// original message size). We should look into making this even more exact.
let big_size_overhead =
BigSize(expected_len as u64).serialized_length().saturating_sub(1);
expected_len.saturating_sub(big_size_overhead)
});

// We always add type and length headers so unpadded messages just at
// PADDING_THRESHOLD_BYTES don't stand out.
BigSize(u64::max_value())
.write(buffer)
.expect("In-memory messages must never fail to serialize");
BigSize(padding_len as u64)
.write(buffer)
.expect("In-memory messages must never fail to serialize");
let mut bytes_written: usize = 0;
while bytes_written < padding_len {
// Write padding in 32-byte chunks if possible.
const PAD_BYTES_LEN: usize = 32;
let pad_bytes = self.padding_entropy_source.get_secure_random_bytes();
let bytes_to_write = (padding_len - bytes_written).min(PAD_BYTES_LEN);
buffer
.write_all(&pad_bytes[..bytes_to_write])
.expect("In-memory messages must never fail to serialize");
bytes_written += bytes_to_write;
}

#[cfg(debug_assertions)]
if orig_buffer_len < PADDING_THRESHOLD_BYTES {
debug_assert_eq!(buffer.len(), PADDING_THRESHOLD_BYTES + 9 + 1);
}
}

/// Encrypts the given pre-serialized message, returning the encrypted version.
/// panics if msg.len() > 65535 or Noise handshake has not finished.
pub fn encrypt_buffer(&mut self, mut msg: MessageBuf) -> Vec<u8> {
pub fn encrypt_buffer(&mut self, mut msg: MessageBuf, should_pad: bool) -> Vec<u8> {
if should_pad {
self.maybe_add_message_padding(&mut msg.0);
}
self.encrypt_message_with_header_0s(&mut msg.0);
msg.0
}

/// Encrypts the given message, returning the encrypted version.
/// panics if the length of `message`, once encoded, is greater than 65535 or if the Noise
/// handshake has not finished.
pub fn encrypt_message<M: wire::Type>(&mut self, message: &M) -> Vec<u8> {
pub fn encrypt_message<M: wire::Type>(&mut self, message: &M, should_pad: bool) -> Vec<u8> {
// Allocate a buffer with 2KB, fitting most common messages. Reserve the first 16+2 bytes
// for the 2-byte message type prefix and its MAC.
let mut res = VecWriter(Vec::with_capacity(MSG_BUF_ALLOC_SIZE));
res.0.resize(16 + 2, 0);
wire::write(message, &mut res).expect("In-memory messages must never fail to serialize");

if should_pad {
self.maybe_add_message_padding(&mut res.0);
}
self.encrypt_message_with_header_0s(&mut res.0);
res.0
}
Expand Down Expand Up @@ -1015,7 +1092,7 @@ mod tests {

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

let len_header = res[0..2 + 16].to_vec();
Expand Down Expand Up @@ -1060,7 +1137,7 @@ mod tests {
fn max_message_len_encryption() {
let mut outbound_peer = get_outbound_peer_for_initiator_test_vectors();
let msg = [4u8; LN_MAX_MSG_LEN + 1];
outbound_peer.encrypt_buffer(MessageBuf::from_encoded(&msg));
outbound_peer.encrypt_buffer(MessageBuf::from_encoded(&msg), false);
}

#[test]
Expand Down
25 changes: 22 additions & 3 deletions lightning/src/ln/peer_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1578,9 +1578,14 @@ where
}
if peer.should_buffer_gossip_broadcast() {
if let Some(msg) = peer.gossip_broadcast_buffer.pop_front() {
let should_pad = peer.their_node_id.is_some_and(|(peer_id, _)| {
let our_features = self.init_features(peer_id);
our_features.supports_message_padding()
});

peer.msgs_sent_since_pong += 1;
peer.pending_outbound_buffer
.push_back(peer.channel_encryptor.encrypt_buffer(msg));
.push_back(peer.channel_encryptor.encrypt_buffer(msg, should_pad));
}
}
if peer.should_buffer_gossip_backfill() {
Expand Down Expand Up @@ -1739,8 +1744,18 @@ where
} else {
debug_assert!(false, "node_id should be set by the time we send a message");
}

let message_padding_supported = their_node_id.is_some_and(|peer_id| {
let our_features = self.init_features(peer_id);
our_features.supports_message_padding()
});

// Opt out of message padding for custom messages as we're not certain the application
// layer protocol can handle TLV exteensions.
let should_pad = !is_custom_msg(message.type_id()) && message_padding_supported;
peer.msgs_sent_since_pong += 1;
peer.pending_outbound_buffer.push_back(peer.channel_encryptor.encrypt_message(message));
peer.pending_outbound_buffer
.push_back(peer.channel_encryptor.encrypt_message(message, should_pad));
}

fn do_read_event(
Expand Down Expand Up @@ -3697,6 +3712,10 @@ fn is_gossip_msg(type_id: u16) -> bool {
}
}

fn is_custom_msg(type_id: u16) -> bool {
type_id >= 32768
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -4261,7 +4280,7 @@ mod tests {
peers[0].read_event(&mut fd_dup, &act_three).unwrap();

let not_init_msg = msgs::Ping { ponglen: 4, byteslen: 0 };
let msg_bytes = dup_encryptor.encrypt_message(&not_init_msg);
let msg_bytes = dup_encryptor.encrypt_message(&not_init_msg, false);
assert!(peers[0].read_event(&mut fd_dup, &msg_bytes).is_err());
}

Expand Down
10 changes: 10 additions & 0 deletions lightning/src/util/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,14 @@ pub struct UserConfig {
///
/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
pub reject_inbound_splices: bool,
/// If this set to `true`, then we will negotiate support for padding network messages with our
/// counterparty. If both parties agree, network messages will be padded to a fixed length to
/// improve privacy in the face of an adversary monitoring network traffic.
///
/// Nodes which are heavily bandwidth-restricted might want to set this to `false`.
///
/// Default value: `true`
pub enable_message_padding: bool,
}

impl Default for UserConfig {
Expand All @@ -990,6 +998,7 @@ impl Default for UserConfig {
enable_htlc_hold: false,
hold_outbound_htlcs_at_next_hop: false,
reject_inbound_splices: true,
enable_message_padding: true,
}
}
}
Expand All @@ -1013,6 +1022,7 @@ impl Readable for UserConfig {
hold_outbound_htlcs_at_next_hop: Readable::read(reader)?,
enable_htlc_hold: Readable::read(reader)?,
reject_inbound_splices: Readable::read(reader)?,
enable_message_padding: Readable::read(reader)?,
})
}
}
Expand Down
Loading