From: Valentine Wallace Date: Sat, 28 May 2022 01:31:27 +0000 (-0700) Subject: Support sending onion messages X-Git-Tag: v0.0.111~46^2~6 X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=commitdiff_plain;h=9051c38ebe42e171fd0fcfa22d2b9ff6a1607b3b;p=rust-lightning Support sending onion messages This adds several utilities in service of then adding OnionMessenger::send_onion_message, which can send to either an unblinded pubkey or a blinded route. Sending custom TLVs and sending an onion message containing a reply path are not yet supported. We also need to split the construct_keys_callback macro into two macros to avoid an unused assignment warning. --- diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 914c8e03..ce91d0d0 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -33,14 +33,14 @@ use io::{Cursor, Read}; use core::convert::{AsMut, TryInto}; use core::ops::Deref; -pub(super) struct OnionKeys { +pub(crate) struct OnionKeys { #[cfg(test)] - pub(super) shared_secret: SharedSecret, + pub(crate) shared_secret: SharedSecret, #[cfg(test)] - pub(super) blinding_factor: [u8; 32], - pub(super) ephemeral_pubkey: PublicKey, - pub(super) rho: [u8; 32], - pub(super) mu: [u8; 32], + pub(crate) blinding_factor: [u8; 32], + pub(crate) ephemeral_pubkey: PublicKey, + pub(crate) rho: [u8; 32], + pub(crate) mu: [u8; 32], } #[inline] @@ -52,7 +52,7 @@ pub(crate) fn gen_rho_from_shared_secret(shared_secret: &[u8]) -> [u8; 32] { } #[inline] -pub(super) fn gen_rho_mu_from_shared_secret(shared_secret: &[u8]) -> ([u8; 32], [u8; 32]) { +pub(crate) fn gen_rho_mu_from_shared_secret(shared_secret: &[u8]) -> ([u8; 32], [u8; 32]) { assert_eq!(shared_secret.len(), 32); ({ let mut hmac = HmacEngine::::new(&[0x72, 0x68, 0x6f]); // rho @@ -260,7 +260,23 @@ impl AsMut<[u8]> for FixedSizeOnionPacket { } } -/// panics if route_size_insane(payloads) +pub(crate) fn payloads_serialized_length(payloads: &Vec) -> usize { + payloads.iter().map(|p| p.serialized_length() + 32 /* HMAC */).sum() +} + +/// panics if payloads_serialized_length(payloads) > packet_data_len +pub(crate) fn construct_onion_message_packet>>( + payloads: Vec, onion_keys: Vec, prng_seed: [u8; 32], packet_data_len: usize) -> P +{ + let mut packet_data = vec![0; packet_data_len]; + + let mut chacha = ChaCha20::new(&prng_seed, &[0; 8]); + chacha.process_in_place(&mut packet_data); + + construct_onion_packet_with_init_noise::<_, _>(payloads, onion_keys, packet_data, None) +} + +/// panics if payloads_serialized_length(payloads) > packet_data.len() fn construct_onion_packet_with_init_noise( mut payloads: Vec, onion_keys: Vec, mut packet_data: P::Data, associated_data: Option<&PaymentHash>) -> P { diff --git a/lightning/src/onion_message/blinded_route.rs b/lightning/src/onion_message/blinded_route.rs index be6e2a01..d18372e3 100644 --- a/lightning/src/onion_message/blinded_route.rs +++ b/lightning/src/onion_message/blinded_route.rs @@ -28,25 +28,25 @@ pub struct BlindedRoute { /// message's next hop and forward it along. /// /// [`encrypted_payload`]: BlindedHop::encrypted_payload - introduction_node_id: PublicKey, + pub(super) introduction_node_id: PublicKey, /// Used by the introduction node to decrypt its [`encrypted_payload`] to forward the onion /// message. /// /// [`encrypted_payload`]: BlindedHop::encrypted_payload - blinding_point: PublicKey, + pub(super) blinding_point: PublicKey, /// The hops composing the blinded route. - blinded_hops: Vec, + pub(super) blinded_hops: Vec, } /// Used to construct the blinded hops portion of a blinded route. These hops cannot be identified /// by outside observers and thus can be used to hide the identity of the recipient. pub struct BlindedHop { /// The blinded node id of this hop in a blinded route. - blinded_node_id: PublicKey, + pub(super) blinded_node_id: PublicKey, /// The encrypted payload intended for this hop in a blinded route. // The node sending to this blinded route will later encode this payload into the onion packet for // this hop. - encrypted_payload: Vec, + pub(super) encrypted_payload: Vec, } impl BlindedRoute { @@ -78,7 +78,7 @@ fn blinded_hops( let mut blinded_hops = Vec::with_capacity(unblinded_path.len()); let mut prev_ss_and_blinded_node_id = None; - utils::construct_keys_callback(secp_ctx, unblinded_path, session_priv, |blinded_node_id, _, _, encrypted_payload_ss, unblinded_pk| { + utils::construct_keys_callback(secp_ctx, unblinded_path, None, session_priv, |blinded_node_id, _, _, encrypted_payload_ss, unblinded_pk, _| { if let Some((prev_ss, prev_blinded_node_id)) = prev_ss_and_blinded_node_id { if let Some(pk) = unblinded_pk { let payload = ForwardTlvs { @@ -117,10 +117,10 @@ fn encrypt_payload(payload: P, encrypted_tlvs_ss: [u8; 32]) -> Vec /// route, they are encoded into [`BlindedHop::encrypted_payload`]. pub(crate) struct ForwardTlvs { /// The node id of the next hop in the onion message's path. - next_node_id: PublicKey, + pub(super) next_node_id: PublicKey, /// Senders to a blinded route use this value to concatenate the route they find to the /// introduction node with the blinded route. - next_blinding_override: Option, + pub(super) next_blinding_override: Option, } /// Similar to [`ForwardTlvs`], but these TLVs are for the final node. @@ -128,7 +128,7 @@ pub(crate) struct ReceiveTlvs { /// If `path_id` is `Some`, it is used to identify the blinded route that this onion message is /// sending to. This is useful for receivers to check that said blinded route is being used in /// the right context. - path_id: Option<[u8; 32]>, + pub(super) path_id: Option<[u8; 32]>, } impl Writeable for ForwardTlvs { diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index e2d7b51a..f4cb57f2 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -10,10 +10,14 @@ //! LDK sends, receives, and forwards onion messages via the [`OnionMessenger`]. See its docs for //! more information. -use bitcoin::secp256k1::{self, PublicKey, Secp256k1}; +use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey}; use chain::keysinterface::{InMemorySigner, KeysInterface, KeysManager, Sign}; use ln::msgs; +use ln::onion_utils; +use super::blinded_route::{BlindedRoute, ForwardTlvs, ReceiveTlvs}; +use super::packet::{BIG_PACKET_HOP_DATA_LEN, ForwardControlTlvs, Packet, Payload, ReceiveControlTlvs, SMALL_PACKET_HOP_DATA_LEN}; +use super::utils; use util::logger::Logger; use core::ops::Deref; @@ -37,6 +41,23 @@ pub struct OnionMessenger // custom_handler: CustomHandler, // handles custom onion messages } +/// The destination of an onion message. +pub enum Destination { + /// We're sending this onion message to a node. + Node(PublicKey), + /// We're sending this onion message to a blinded route. + BlindedRoute(BlindedRoute), +} + +impl Destination { + pub(super) fn num_hops(&self) -> usize { + match self { + Destination::Node(_) => 1, + Destination::BlindedRoute(BlindedRoute { blinded_hops, .. }) => blinded_hops.len(), + } + } +} + impl OnionMessenger where K::Target: KeysInterface, L::Target: Logger, @@ -53,6 +74,36 @@ impl OnionMessenger logger, } } + + /// Send an empty onion message to `destination`, routing it through `intermediate_nodes`. + pub fn send_onion_message(&self, intermediate_nodes: &[PublicKey], destination: Destination) -> Result<(), secp256k1::Error> { + let blinding_secret_bytes = self.keys_manager.get_secure_random_bytes(); + let blinding_secret = SecretKey::from_slice(&blinding_secret_bytes[..]).expect("RNG is busted"); + let (introduction_node_id, blinding_point) = if intermediate_nodes.len() != 0 { + (intermediate_nodes[0], PublicKey::from_secret_key(&self.secp_ctx, &blinding_secret)) + } else { + match destination { + Destination::Node(pk) => (pk, PublicKey::from_secret_key(&self.secp_ctx, &blinding_secret)), + Destination::BlindedRoute(BlindedRoute { introduction_node_id, blinding_point, .. }) => + (introduction_node_id, blinding_point), + } + }; + let (packet_payloads, packet_keys) = packet_payloads_and_keys( + &self.secp_ctx, intermediate_nodes, destination, &blinding_secret)?; + + let prng_seed = self.keys_manager.get_secure_random_bytes(); + let onion_packet = construct_onion_message_packet(packet_payloads, packet_keys, prng_seed); + + let mut pending_per_peer_msgs = self.pending_messages.lock().unwrap(); + let pending_msgs = pending_per_peer_msgs.entry(introduction_node_id).or_insert(Vec::new()); + pending_msgs.push( + msgs::OnionMessage { + blinding_point, + onion_routing_packet: onion_packet, + } + ); + Ok(()) + } } // TODO: parameterize the below Simple* types with OnionMessenger and handle the messages it @@ -69,3 +120,90 @@ pub type SimpleArcOnionMessenger = OnionMessenger = OnionMessenger; + +/// Construct onion packet payloads and keys for sending an onion message along the given +/// `unblinded_path` to the given `destination`. +fn packet_payloads_and_keys( + secp_ctx: &Secp256k1, unblinded_path: &[PublicKey], destination: Destination, session_priv: &SecretKey +) -> Result<(Vec<(Payload, [u8; 32])>, Vec), secp256k1::Error> { + let num_hops = unblinded_path.len() + destination.num_hops(); + let mut payloads = Vec::with_capacity(num_hops); + let mut onion_packet_keys = Vec::with_capacity(num_hops); + + let (mut intro_node_id_blinding_pt, num_blinded_hops) = if let Destination::BlindedRoute(BlindedRoute { + introduction_node_id, blinding_point, blinded_hops }) = &destination { + (Some((*introduction_node_id, *blinding_point)), blinded_hops.len()) } else { (None, 0) }; + let num_unblinded_hops = num_hops - num_blinded_hops; + + let mut unblinded_path_idx = 0; + let mut blinded_path_idx = 0; + let mut prev_control_tlvs_ss = None; + utils::construct_keys_callback(secp_ctx, unblinded_path, Some(destination), session_priv, |_, onion_packet_ss, ephemeral_pubkey, control_tlvs_ss, unblinded_pk_opt, enc_payload_opt| { + if num_unblinded_hops != 0 && unblinded_path_idx < num_unblinded_hops { + if let Some(ss) = prev_control_tlvs_ss.take() { + payloads.push((Payload::Forward(ForwardControlTlvs::Unblinded( + ForwardTlvs { + next_node_id: unblinded_pk_opt.unwrap(), + next_blinding_override: None, + } + )), ss)); + } + prev_control_tlvs_ss = Some(control_tlvs_ss); + unblinded_path_idx += 1; + } else if let Some((intro_node_id, blinding_pt)) = intro_node_id_blinding_pt.take() { + if let Some(control_tlvs_ss) = prev_control_tlvs_ss.take() { + payloads.push((Payload::Forward(ForwardControlTlvs::Unblinded(ForwardTlvs { + next_node_id: intro_node_id, + next_blinding_override: Some(blinding_pt), + })), control_tlvs_ss)); + } + if let Some(encrypted_payload) = enc_payload_opt { + payloads.push((Payload::Forward(ForwardControlTlvs::Blinded(encrypted_payload)), + control_tlvs_ss)); + } else { debug_assert!(false); } + blinded_path_idx += 1; + } else if blinded_path_idx < num_blinded_hops - 1 && enc_payload_opt.is_some() { + payloads.push((Payload::Forward(ForwardControlTlvs::Blinded(enc_payload_opt.unwrap())), + control_tlvs_ss)); + blinded_path_idx += 1; + } else if let Some(encrypted_payload) = enc_payload_opt { + payloads.push((Payload::Receive { + control_tlvs: ReceiveControlTlvs::Blinded(encrypted_payload), + }, control_tlvs_ss)); + } + + let (rho, mu) = onion_utils::gen_rho_mu_from_shared_secret(onion_packet_ss.as_ref()); + onion_packet_keys.push(onion_utils::OnionKeys { + #[cfg(test)] + shared_secret: onion_packet_ss, + #[cfg(test)] + blinding_factor: [0; 32], + ephemeral_pubkey, + rho, + mu, + }); + })?; + + if let Some(control_tlvs_ss) = prev_control_tlvs_ss { + payloads.push((Payload::Receive { + control_tlvs: ReceiveControlTlvs::Unblinded(ReceiveTlvs { path_id: None, }) + }, control_tlvs_ss)); + } + + Ok((payloads, onion_packet_keys)) +} + +fn construct_onion_message_packet(payloads: Vec<(Payload, [u8; 32])>, onion_keys: Vec, prng_seed: [u8; 32]) -> Packet { + // Spec rationale: + // "`len` allows larger messages to be sent than the standard 1300 bytes allowed for an HTLC + // onion, but this should be used sparingly as it is reduces anonymity set, hence the + // recommendation that it either look like an HTLC onion, or if larger, be a fixed size." + let payloads_ser_len = onion_utils::payloads_serialized_length(&payloads); + let hop_data_len = if payloads_ser_len <= SMALL_PACKET_HOP_DATA_LEN { + SMALL_PACKET_HOP_DATA_LEN + } else if payloads_ser_len <= BIG_PACKET_HOP_DATA_LEN { + BIG_PACKET_HOP_DATA_LEN + } else { payloads_ser_len }; + + onion_utils::construct_onion_message_packet::<_, _>(payloads, onion_keys, prng_seed, hop_data_len) +} diff --git a/lightning/src/onion_message/mod.rs b/lightning/src/onion_message/mod.rs index 291030f4..9236341f 100644 --- a/lightning/src/onion_message/mod.rs +++ b/lightning/src/onion_message/mod.rs @@ -16,5 +16,5 @@ mod utils; // Re-export structs so they can be imported with just the `onion_message::` module prefix. pub use self::blinded_route::{BlindedRoute, BlindedHop}; -pub use self::messenger::{OnionMessenger, SimpleArcOnionMessenger, SimpleRefOnionMessenger}; +pub use self::messenger::{Destination, OnionMessenger, SimpleArcOnionMessenger, SimpleRefOnionMessenger}; pub(crate) use self::packet::Packet; diff --git a/lightning/src/onion_message/packet.rs b/lightning/src/onion_message/packet.rs index 37f178f5..ed0206a2 100644 --- a/lightning/src/onion_message/packet.rs +++ b/lightning/src/onion_message/packet.rs @@ -13,12 +13,19 @@ use bitcoin::secp256k1::PublicKey; use ln::msgs::DecodeError; use ln::onion_utils; +use super::blinded_route::{ForwardTlvs, ReceiveTlvs}; +use util::chacha20poly1305rfc::ChaChaPolyWriteAdapter; use util::ser::{LengthRead, LengthReadable, Readable, Writeable, Writer}; use core::cmp; use io; use prelude::*; +// Per the spec, an onion message packet's `hop_data` field length should be +// SMALL_PACKET_HOP_DATA_LEN if it fits, else BIG_PACKET_HOP_DATA_LEN if it fits. +pub(super) const SMALL_PACKET_HOP_DATA_LEN: usize = 1300; +pub(super) const BIG_PACKET_HOP_DATA_LEN: usize = 32768; + #[derive(Clone, Debug, PartialEq)] pub(crate) struct Packet { version: u8, @@ -80,3 +87,72 @@ impl LengthReadable for Packet { }) } } + +/// Onion message payloads contain "control" TLVs and "data" TLVs. Control TLVs are used to route +/// the onion message from hop to hop and for path verification, whereas data TLVs contain the onion +/// message content itself, such as an invoice request. +pub(super) enum Payload { + /// This payload is for an intermediate hop. + Forward(ForwardControlTlvs), + /// This payload is for the final hop. + Receive { + control_tlvs: ReceiveControlTlvs, + // Coming soon: + // reply_path: Option, + // message: Message, + } +} + +// Coming soon: +// enum Message { +// InvoiceRequest(InvoiceRequest), +// Invoice(Invoice), +// InvoiceError(InvoiceError), +// CustomMessage, +// } + +/// Forward control TLVs in their blinded and unblinded form. +pub(super) enum ForwardControlTlvs { + /// If we're sending to a blinded route, the node that constructed the blinded route has provided + /// this hop's control TLVs, already encrypted into bytes. + Blinded(Vec), + /// If we're constructing an onion message hop through an intermediate unblinded node, we'll need + /// to construct the intermediate hop's control TLVs in their unblinded state to avoid encoding + /// them into an intermediate Vec. See [`super::blinded_route::ForwardTlvs`] for more info. + Unblinded(ForwardTlvs), +} + +/// Receive control TLVs in their blinded and unblinded form. +pub(super) enum ReceiveControlTlvs { + /// See [`ForwardControlTlvs::Blinded`]. + Blinded(Vec), + /// See [`ForwardControlTlvs::Unblinded`] and [`super::blinded_route::ReceiveTlvs`]. + Unblinded(ReceiveTlvs), +} + +// Uses the provided secret to simultaneously encode and encrypt the unblinded control TLVs. +impl Writeable for (Payload, [u8; 32]) { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + match &self.0 { + Payload::Forward(ForwardControlTlvs::Blinded(encrypted_bytes)) | + Payload::Receive { control_tlvs: ReceiveControlTlvs::Blinded(encrypted_bytes)} => { + encode_varint_length_prefixed_tlv!(w, { + (4, encrypted_bytes, vec_type) + }) + }, + Payload::Forward(ForwardControlTlvs::Unblinded(control_tlvs)) => { + let write_adapter = ChaChaPolyWriteAdapter::new(self.1, &control_tlvs); + encode_varint_length_prefixed_tlv!(w, { + (4, write_adapter, required) + }) + }, + Payload::Receive { control_tlvs: ReceiveControlTlvs::Unblinded(control_tlvs)} => { + let write_adapter = ChaChaPolyWriteAdapter::new(self.1, &control_tlvs); + encode_varint_length_prefixed_tlv!(w, { + (4, write_adapter, required) + }) + }, + } + Ok(()) + } +} diff --git a/lightning/src/onion_message/utils.rs b/lightning/src/onion_message/utils.rs index 785a373c..9b95183e 100644 --- a/lightning/src/onion_message/utils.rs +++ b/lightning/src/onion_message/utils.rs @@ -16,14 +16,16 @@ use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey}; use bitcoin::secp256k1::ecdh::SharedSecret; use ln::onion_utils; +use super::blinded_route::BlindedRoute; +use super::messenger::Destination; use prelude::*; // TODO: DRY with onion_utils::construct_onion_keys_callback #[inline] pub(super) fn construct_keys_callback)>( - secp_ctx: &Secp256k1, unblinded_path: &[PublicKey], + FType: FnMut(PublicKey, SharedSecret, PublicKey, [u8; 32], Option, Option>)>( + secp_ctx: &Secp256k1, unblinded_path: &[PublicKey], destination: Option, session_priv: &SecretKey, mut callback: FType ) -> Result<(), secp256k1::Error> { let mut msg_blinding_point_priv = session_priv.clone(); @@ -32,7 +34,7 @@ pub(super) fn construct_keys_callback { + ($pk: expr, $blinded: expr, $encrypted_payload: expr) => {{ let encrypted_data_ss = SharedSecret::new(&$pk, &msg_blinding_point_priv); let blinded_hop_pk = if $blinded { $pk } else { @@ -49,7 +51,14 @@ pub(super) fn construct_keys_callback { + let (encrypted_data_ss, onion_packet_ss) = build_keys!($pk, $blinded, $encrypted_payload); let msg_blinding_point_blinding_factor = { let mut sha = Sha256::engine(); @@ -73,7 +82,19 @@ pub(super) fn construct_keys_callback { + build_keys!(pk, false, None); + }, + Destination::BlindedRoute(BlindedRoute { blinded_hops, .. }) => { + for hop in blinded_hops { + build_keys_in_loop!(hop.blinded_node_id, true, Some(hop.encrypted_payload)); + } + }, + } } Ok(()) }