From 45a0b5e100b16699761859a762434df1d650f72e Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Tue, 10 Jun 2025 23:59:36 +0000 Subject: [PATCH 1/8] Make `Poly1305::result` return, rather than copy to a slice `Poly1305::raw_result` copies the output into a slice, for some reason allowing any length sice. This isn't a great API, so while we're here we change it to return the 16-byte tag instead. --- lightning/src/crypto/chacha20poly1305rfc.rs | 10 ++++------ lightning/src/crypto/poly1305.rs | 13 +++++++------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/lightning/src/crypto/chacha20poly1305rfc.rs b/lightning/src/crypto/chacha20poly1305rfc.rs index f1c261cb1f1..7c7cf245460 100644 --- a/lightning/src/crypto/chacha20poly1305rfc.rs +++ b/lightning/src/crypto/chacha20poly1305rfc.rs @@ -67,7 +67,7 @@ mod real_chachapoly { self.finished = true; self.mac.input(&self.aad_len.to_le_bytes()); self.mac.input(&(self.data_len as u64).to_le_bytes()); - self.mac.raw_result(out_tag); + out_tag.copy_from_slice(&self.mac.result()); } pub fn encrypt_full_message_in_place( @@ -94,7 +94,7 @@ mod real_chachapoly { self.finished = true; self.mac.input(&self.aad_len.to_le_bytes()); self.mac.input(&(self.data_len as u64).to_le_bytes()); - self.mac.raw_result(out_tag); + out_tag.copy_from_slice(&self.mac.result()); } /// Decrypt the `input`, checking the given `tag` prior to writing the decrypted contents @@ -115,8 +115,7 @@ mod real_chachapoly { self.mac.input(&self.aad_len.to_le_bytes()); self.mac.input(&(self.data_len as u64).to_le_bytes()); - let mut calc_tag = [0u8; 16]; - self.mac.raw_result(&mut calc_tag); + let calc_tag = self.mac.result(); if fixed_time_eq(&calc_tag, tag) { self.cipher.process(input, output); Ok(()) @@ -156,8 +155,7 @@ mod real_chachapoly { self.mac.input(&self.aad_len.to_le_bytes()); self.mac.input(&(self.data_len as u64).to_le_bytes()); - let mut calc_tag = [0u8; 16]; - self.mac.raw_result(&mut calc_tag); + let calc_tag = self.mac.result(); if fixed_time_eq(&calc_tag, tag) { true } else { diff --git a/lightning/src/crypto/poly1305.rs b/lightning/src/crypto/poly1305.rs index 6ac1c6c9694..e500c5fa79f 100644 --- a/lightning/src/crypto/poly1305.rs +++ b/lightning/src/crypto/poly1305.rs @@ -252,15 +252,16 @@ impl Poly1305 { self.leftover = m.len(); } - pub fn raw_result(&mut self, output: &mut [u8]) { - assert!(output.len() >= 16); + pub fn result(&mut self) -> [u8; 16] { if !self.finalized { self.finish(); } + let mut output = [0; 16]; output[0..4].copy_from_slice(&self.h[0].to_le_bytes()); output[4..8].copy_from_slice(&self.h[1].to_le_bytes()); output[8..12].copy_from_slice(&self.h[2].to_le_bytes()); output[12..16].copy_from_slice(&self.h[3].to_le_bytes()); + output } } @@ -270,10 +271,10 @@ mod test { use super::Poly1305; - fn poly1305(key: &[u8], msg: &[u8], mac: &mut [u8]) { + fn poly1305(key: &[u8], msg: &[u8], mac: &mut [u8; 16]) { let mut poly = Poly1305::new(key); poly.input(msg); - poly.raw_result(mac); + *mac = poly.result(); } #[test] @@ -318,7 +319,7 @@ mod test { poly.input(&msg[128..129]); poly.input(&msg[129..130]); poly.input(&msg[130..131]); - poly.raw_result(&mut mac); + let mac = poly.result(); assert_eq!(&mac[..], &expected[..]); } @@ -363,7 +364,7 @@ mod test { poly1305(&key[..], &msg[0..i], &mut mac); tpoly.input(&mac); } - tpoly.raw_result(&mut mac); + let mac = tpoly.result(); assert_eq!(&mac[..], &total_mac[..]); } From 15a88712dbc9e815c29a0484c50d5f849b041e35 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 26 Jun 2025 11:18:03 +0000 Subject: [PATCH 2/8] Move `Poly1305` fuzzing logic from `chacha20poly1305rfc.rs` Rather than skipping compilation of `poly1305.rs` when building for fuzzing and relying on `ChaCha20Poly1305` to do the fuzzing variants, implement an actual fuzz wrapper in `poly1305.rs`, keeping the same fuzz MAC structure that we already have. We also add a fuzzing implementation of `fixed_time_eq` which does a simple comparison, to allow the fuzzer to "see into" the comparison in some cases. Best reviewed with `-b`. --- lightning/src/crypto/chacha20poly1305rfc.rs | 343 ++++----- lightning/src/crypto/mod.rs | 9 +- lightning/src/crypto/poly1305.rs | 746 +++++++++++--------- 3 files changed, 522 insertions(+), 576 deletions(-) diff --git a/lightning/src/crypto/chacha20poly1305rfc.rs b/lightning/src/crypto/chacha20poly1305rfc.rs index 7c7cf245460..839fad9ce6c 100644 --- a/lightning/src/crypto/chacha20poly1305rfc.rs +++ b/lightning/src/crypto/chacha20poly1305rfc.rs @@ -10,247 +10,148 @@ // This is a port of Andrew Moons poly1305-donna // https://github.com/floodyberry/poly1305-donna -#[cfg(not(fuzzing))] -mod real_chachapoly { - use super::super::chacha20::ChaCha20; - use super::super::fixed_time_eq; - use super::super::poly1305::Poly1305; - - #[derive(Clone, Copy)] - pub struct ChaCha20Poly1305RFC { - cipher: ChaCha20, - mac: Poly1305, - finished: bool, - data_len: usize, - aad_len: u64, - } - - impl ChaCha20Poly1305RFC { - #[inline] - fn pad_mac_16(mac: &mut Poly1305, len: usize) { - if len % 16 != 0 { - mac.input(&[0; 16][0..16 - (len % 16)]); - } - } - pub fn new(key: &[u8], nonce: &[u8], aad: &[u8]) -> ChaCha20Poly1305RFC { - assert!(key.len() == 16 || key.len() == 32); - assert!(nonce.len() == 12); - - // Ehh, I'm too lazy to *also* tweak ChaCha20 to make it RFC-compliant - assert!(nonce[0] == 0 && nonce[1] == 0 && nonce[2] == 0 && nonce[3] == 0); - - let mut cipher = ChaCha20::new(key, &nonce[4..]); - let mut mac_key = [0u8; 64]; - let zero_key = [0u8; 64]; - cipher.process(&zero_key, &mut mac_key); - - let mut mac = Poly1305::new(&mac_key[..32]); - mac.input(aad); - ChaCha20Poly1305RFC::pad_mac_16(&mut mac, aad.len()); - - ChaCha20Poly1305RFC { - cipher, - mac, - finished: false, - data_len: 0, - aad_len: aad.len() as u64, - } - } - - pub fn encrypt(&mut self, input: &[u8], output: &mut [u8], out_tag: &mut [u8]) { - assert!(input.len() == output.len()); - assert!(!self.finished); - self.cipher.process(input, output); - self.data_len += input.len(); - self.mac.input(output); - ChaCha20Poly1305RFC::pad_mac_16(&mut self.mac, self.data_len); - self.finished = true; - self.mac.input(&self.aad_len.to_le_bytes()); - self.mac.input(&(self.data_len as u64).to_le_bytes()); - out_tag.copy_from_slice(&self.mac.result()); - } - - pub fn encrypt_full_message_in_place( - &mut self, input_output: &mut [u8], out_tag: &mut [u8], - ) { - self.encrypt_in_place(input_output); - self.finish_and_get_tag(out_tag); - } - - // Encrypt `input_output` in-place. To finish and calculate the tag, use `finish_and_get_tag` - // below. - pub(in super::super) fn encrypt_in_place(&mut self, input_output: &mut [u8]) { - debug_assert!(!self.finished); - self.cipher.process_in_place(input_output); - self.data_len += input_output.len(); - self.mac.input(input_output); - } - - // If we were previously encrypting with `encrypt_in_place`, this method can be used to finish - // encrypting and calculate the tag. - pub(in super::super) fn finish_and_get_tag(&mut self, out_tag: &mut [u8]) { - debug_assert!(!self.finished); - ChaCha20Poly1305RFC::pad_mac_16(&mut self.mac, self.data_len); - self.finished = true; - self.mac.input(&self.aad_len.to_le_bytes()); - self.mac.input(&(self.data_len as u64).to_le_bytes()); - out_tag.copy_from_slice(&self.mac.result()); - } - - /// Decrypt the `input`, checking the given `tag` prior to writing the decrypted contents - /// into `output`. Note that, because `output` is not touched until the `tag` is checked, - /// this decryption is *variable time*. - pub fn variable_time_decrypt( - &mut self, input: &[u8], output: &mut [u8], tag: &[u8], - ) -> Result<(), ()> { - assert!(input.len() == output.len()); - assert!(!self.finished); - - self.finished = true; - - self.mac.input(input); - - self.data_len += input.len(); - ChaCha20Poly1305RFC::pad_mac_16(&mut self.mac, self.data_len); - self.mac.input(&self.aad_len.to_le_bytes()); - self.mac.input(&(self.data_len as u64).to_le_bytes()); - - let calc_tag = self.mac.result(); - if fixed_time_eq(&calc_tag, tag) { - self.cipher.process(input, output); - Ok(()) - } else { - Err(()) - } - } - - pub fn check_decrypt_in_place( - &mut self, input_output: &mut [u8], tag: &[u8], - ) -> Result<(), ()> { - self.decrypt_in_place(input_output); - if self.finish_and_check_tag(tag) { - Ok(()) - } else { - Err(()) - } - } - - /// Decrypt in place, without checking the tag. Use `finish_and_check_tag` to check it - /// later when decryption finishes. - /// - /// Should never be `pub` because the public API should always enforce tag checking. - pub(in super::super) fn decrypt_in_place(&mut self, input_output: &mut [u8]) { - debug_assert!(!self.finished); - self.mac.input(input_output); - self.data_len += input_output.len(); - self.cipher.process_in_place(input_output); - } - - /// If we were previously decrypting with `just_decrypt_in_place`, this method must be used - /// to check the tag. Returns whether or not the tag is valid. - pub(in super::super) fn finish_and_check_tag(&mut self, tag: &[u8]) -> bool { - debug_assert!(!self.finished); - self.finished = true; - ChaCha20Poly1305RFC::pad_mac_16(&mut self.mac, self.data_len); - self.mac.input(&self.aad_len.to_le_bytes()); - self.mac.input(&(self.data_len as u64).to_le_bytes()); +use super::chacha20::ChaCha20; +use super::fixed_time_eq; +use super::poly1305::Poly1305; + +pub struct ChaCha20Poly1305RFC { + cipher: ChaCha20, + mac: Poly1305, + finished: bool, + data_len: usize, + aad_len: u64, +} - let calc_tag = self.mac.result(); - if fixed_time_eq(&calc_tag, tag) { - true - } else { - false - } +impl ChaCha20Poly1305RFC { + #[inline] + fn pad_mac_16(mac: &mut Poly1305, len: usize) { + if len % 16 != 0 { + mac.input(&[0; 16][0..16 - (len % 16)]); } } -} -#[cfg(not(fuzzing))] -pub use self::real_chachapoly::ChaCha20Poly1305RFC; - -#[cfg(fuzzing)] -mod fuzzy_chachapoly { - #[derive(Clone, Copy)] - pub struct ChaCha20Poly1305RFC { - tag: [u8; 16], - finished: bool, + pub fn new(key: &[u8], nonce: &[u8], aad: &[u8]) -> ChaCha20Poly1305RFC { + assert!(key.len() == 16 || key.len() == 32); + assert!(nonce.len() == 12); + + // Ehh, I'm too lazy to *also* tweak ChaCha20 to make it RFC-compliant + assert!(nonce[0] == 0 && nonce[1] == 0 && nonce[2] == 0 && nonce[3] == 0); + + let mut cipher = ChaCha20::new(key, &nonce[4..]); + let mut mac_key = [0u8; 64]; + let zero_key = [0u8; 64]; + cipher.process(&zero_key, &mut mac_key); + + #[cfg(not(fuzzing))] + let mut mac = Poly1305::new(&mac_key[..32]); + #[cfg(fuzzing)] + let mut mac = Poly1305::new(&key); + mac.input(aad); + ChaCha20Poly1305RFC::pad_mac_16(&mut mac, aad.len()); + + ChaCha20Poly1305RFC { cipher, mac, finished: false, data_len: 0, aad_len: aad.len() as u64 } } - impl ChaCha20Poly1305RFC { - pub fn new(key: &[u8], nonce: &[u8], _aad: &[u8]) -> ChaCha20Poly1305RFC { - assert!(key.len() == 16 || key.len() == 32); - assert!(nonce.len() == 12); - - // Ehh, I'm too lazy to *also* tweak ChaCha20 to make it RFC-compliant - assert!(nonce[0] == 0 && nonce[1] == 0 && nonce[2] == 0 && nonce[3] == 0); - let mut tag = [0; 16]; - tag.copy_from_slice(&key[0..16]); + pub fn encrypt(&mut self, input: &[u8], output: &mut [u8], out_tag: &mut [u8]) { + assert!(input.len() == output.len()); + assert!(!self.finished); + self.cipher.process(input, output); + self.data_len += input.len(); + self.mac.input(output); + ChaCha20Poly1305RFC::pad_mac_16(&mut self.mac, self.data_len); + self.finished = true; + self.mac.input(&self.aad_len.to_le_bytes()); + self.mac.input(&(self.data_len as u64).to_le_bytes()); + out_tag.copy_from_slice(&self.mac.result()); + } - ChaCha20Poly1305RFC { tag, finished: false } - } + pub fn encrypt_full_message_in_place(&mut self, input_output: &mut [u8], out_tag: &mut [u8]) { + self.encrypt_in_place(input_output); + self.finish_and_get_tag(out_tag); + } - pub fn encrypt(&mut self, input: &[u8], output: &mut [u8], out_tag: &mut [u8]) { - assert!(input.len() == output.len()); - assert!(self.finished == false); + // Encrypt `input_output` in-place. To finish and calculate the tag, use `finish_and_get_tag` + // below. + pub(in super::super) fn encrypt_in_place(&mut self, input_output: &mut [u8]) { + debug_assert!(!self.finished); + self.cipher.process_in_place(input_output); + self.data_len += input_output.len(); + self.mac.input(input_output); + } - output.copy_from_slice(&input); - out_tag.copy_from_slice(&self.tag); - self.finished = true; - } + // If we were previously encrypting with `encrypt_in_place`, this method can be used to finish + // encrypting and calculate the tag. + pub(in super::super) fn finish_and_get_tag(&mut self, out_tag: &mut [u8]) { + debug_assert!(!self.finished); + ChaCha20Poly1305RFC::pad_mac_16(&mut self.mac, self.data_len); + self.finished = true; + self.mac.input(&self.aad_len.to_le_bytes()); + self.mac.input(&(self.data_len as u64).to_le_bytes()); + out_tag.copy_from_slice(&self.mac.result()); + } - pub fn encrypt_full_message_in_place( - &mut self, input_output: &mut [u8], out_tag: &mut [u8], - ) { - self.encrypt_in_place(input_output); - self.finish_and_get_tag(out_tag); - } + /// Decrypt the `input`, checking the given `tag` prior to writing the decrypted contents + /// into `output`. Note that, because `output` is not touched until the `tag` is checked, + /// this decryption is *variable time*. + pub fn variable_time_decrypt( + &mut self, input: &[u8], output: &mut [u8], tag: &[u8], + ) -> Result<(), ()> { + assert!(input.len() == output.len()); + assert!(!self.finished); - pub(in super::super) fn encrypt_in_place(&mut self, _input_output: &mut [u8]) { - assert!(self.finished == false); - } + self.finished = true; - pub(in super::super) fn finish_and_get_tag(&mut self, out_tag: &mut [u8]) { - assert!(self.finished == false); - out_tag.copy_from_slice(&self.tag); - self.finished = true; - } + self.mac.input(input); - pub fn variable_time_decrypt( - &mut self, input: &[u8], output: &mut [u8], tag: &[u8], - ) -> Result<(), ()> { - assert!(input.len() == output.len()); - assert!(self.finished == false); + self.data_len += input.len(); + ChaCha20Poly1305RFC::pad_mac_16(&mut self.mac, self.data_len); + self.mac.input(&self.aad_len.to_le_bytes()); + self.mac.input(&(self.data_len as u64).to_le_bytes()); - if tag[..] != self.tag[..] { - return Err(()); - } - output.copy_from_slice(input); - self.finished = true; + let calc_tag = self.mac.result(); + if fixed_time_eq(&calc_tag, tag) { + self.cipher.process(input, output); Ok(()) + } else { + Err(()) } + } - pub fn check_decrypt_in_place( - &mut self, input_output: &mut [u8], tag: &[u8], - ) -> Result<(), ()> { - self.decrypt_in_place(input_output); - if self.finish_and_check_tag(tag) { - Ok(()) - } else { - Err(()) - } + pub fn check_decrypt_in_place( + &mut self, input_output: &mut [u8], tag: &[u8], + ) -> Result<(), ()> { + self.decrypt_in_place(input_output); + if self.finish_and_check_tag(tag) { + Ok(()) + } else { + Err(()) } + } - pub(in super::super) fn decrypt_in_place(&mut self, _input: &mut [u8]) { - assert!(self.finished == false); - } + /// Decrypt in place, without checking the tag. Use `finish_and_check_tag` to check it + /// later when decryption finishes. + /// + /// Should never be `pub` because the public API should always enforce tag checking. + pub(in super::super) fn decrypt_in_place(&mut self, input_output: &mut [u8]) { + debug_assert!(!self.finished); + self.mac.input(input_output); + self.data_len += input_output.len(); + self.cipher.process_in_place(input_output); + } - pub(in super::super) fn finish_and_check_tag(&mut self, tag: &[u8]) -> bool { - if tag[..] != self.tag[..] { - return false; - } - self.finished = true; + /// If we were previously decrypting with `just_decrypt_in_place`, this method must be used + /// to check the tag. Returns whether or not the tag is valid. + pub(in super::super) fn finish_and_check_tag(&mut self, tag: &[u8]) -> bool { + debug_assert!(!self.finished); + self.finished = true; + ChaCha20Poly1305RFC::pad_mac_16(&mut self.mac, self.data_len); + self.mac.input(&self.aad_len.to_le_bytes()); + self.mac.input(&(self.data_len as u64).to_le_bytes()); + + let calc_tag = self.mac.result(); + if fixed_time_eq(&calc_tag, tag) { true + } else { + false } } } -#[cfg(fuzzing)] -pub use self::fuzzy_chachapoly::ChaCha20Poly1305RFC; diff --git a/lightning/src/crypto/mod.rs b/lightning/src/crypto/mod.rs index 4dc851a3683..478918a49a8 100644 --- a/lightning/src/crypto/mod.rs +++ b/lightning/src/crypto/mod.rs @@ -1,9 +1,14 @@ #[cfg(not(fuzzing))] -use bitcoin::hashes::cmp::fixed_time_eq; +pub(crate) use bitcoin::hashes::cmp::fixed_time_eq; + +#[cfg(fuzzing)] +fn fixed_time_eq(a: &[u8], b: &[u8]) -> bool { + assert_eq!(a.len(), b.len()); + a == b +} pub(crate) mod chacha20; pub(crate) mod chacha20poly1305rfc; -#[cfg(not(fuzzing))] pub(crate) mod poly1305; pub(crate) mod streams; pub(crate) mod utils; diff --git a/lightning/src/crypto/poly1305.rs b/lightning/src/crypto/poly1305.rs index e500c5fa79f..c7306863e9e 100644 --- a/lightning/src/crypto/poly1305.rs +++ b/lightning/src/crypto/poly1305.rs @@ -7,386 +7,426 @@ // This is a port of Andrew Moons poly1305-donna // https://github.com/floodyberry/poly1305-donna -use core::cmp::min; - -use crate::prelude::*; - -#[derive(Clone, Copy)] -pub struct Poly1305 { - r: [u32; 5], - h: [u32; 5], - pad: [u32; 4], - leftover: usize, - buffer: [u8; 16], - finalized: bool, -} - -impl Poly1305 { - pub fn new(key: &[u8]) -> Poly1305 { - assert!(key.len() == 32); - let mut poly = Poly1305 { - r: [0u32; 5], - h: [0u32; 5], - pad: [0u32; 4], - leftover: 0, - buffer: [0u8; 16], - finalized: false, - }; - - // r &= 0xffffffc0ffffffc0ffffffc0fffffff - poly.r[0] = (u32::from_le_bytes(key[0..4].try_into().expect("len is 4"))) & 0x3ffffff; - poly.r[1] = (u32::from_le_bytes(key[3..7].try_into().expect("len is 4")) >> 2) & 0x3ffff03; - poly.r[2] = (u32::from_le_bytes(key[6..10].try_into().expect("len is 4")) >> 4) & 0x3ffc0ff; - poly.r[3] = (u32::from_le_bytes(key[9..13].try_into().expect("len is 4")) >> 6) & 0x3f03fff; - poly.r[4] = - (u32::from_le_bytes(key[12..16].try_into().expect("len is 4")) >> 8) & 0x00fffff; - - poly.pad[0] = u32::from_le_bytes(key[16..20].try_into().expect("len is 4")); - poly.pad[1] = u32::from_le_bytes(key[20..24].try_into().expect("len is 4")); - poly.pad[2] = u32::from_le_bytes(key[24..28].try_into().expect("len is 4")); - poly.pad[3] = u32::from_le_bytes(key[28..32].try_into().expect("len is 4")); - - poly +#[cfg(not(fuzzing))] +mod fuzzy_poly1305 { + use core::cmp::min; + + #[derive(Clone, Copy)] + pub struct Poly1305 { + r: [u32; 5], + h: [u32; 5], + pad: [u32; 4], + leftover: usize, + buffer: [u8; 16], + finalized: bool, } - fn block(&mut self, m: &[u8]) { - let hibit: u32 = if self.finalized { 0 } else { 1 << 24 }; - - let r0 = self.r[0]; - let r1 = self.r[1]; - let r2 = self.r[2]; - let r3 = self.r[3]; - let r4 = self.r[4]; - - let s1 = r1 * 5; - let s2 = r2 * 5; - let s3 = r3 * 5; - let s4 = r4 * 5; - - let mut h0 = self.h[0]; - let mut h1 = self.h[1]; - let mut h2 = self.h[2]; - let mut h3 = self.h[3]; - let mut h4 = self.h[4]; - - // h += m - h0 += (u32::from_le_bytes(m[0..4].try_into().expect("len is 4"))) & 0x3ffffff; - h1 += (u32::from_le_bytes(m[3..7].try_into().expect("len is 4")) >> 2) & 0x3ffffff; - h2 += (u32::from_le_bytes(m[6..10].try_into().expect("len is 4")) >> 4) & 0x3ffffff; - h3 += (u32::from_le_bytes(m[9..13].try_into().expect("len is 4")) >> 6) & 0x3ffffff; - h4 += (u32::from_le_bytes(m[12..16].try_into().expect("len is 4")) >> 8) | hibit; - - // h *= r - let d0 = (h0 as u64 * r0 as u64) - + (h1 as u64 * s4 as u64) - + (h2 as u64 * s3 as u64) - + (h3 as u64 * s2 as u64) - + (h4 as u64 * s1 as u64); - let mut d1 = (h0 as u64 * r1 as u64) - + (h1 as u64 * r0 as u64) - + (h2 as u64 * s4 as u64) - + (h3 as u64 * s3 as u64) - + (h4 as u64 * s2 as u64); - let mut d2 = (h0 as u64 * r2 as u64) - + (h1 as u64 * r1 as u64) - + (h2 as u64 * r0 as u64) - + (h3 as u64 * s4 as u64) - + (h4 as u64 * s3 as u64); - let mut d3 = (h0 as u64 * r3 as u64) - + (h1 as u64 * r2 as u64) - + (h2 as u64 * r1 as u64) - + (h3 as u64 * r0 as u64) - + (h4 as u64 * s4 as u64); - let mut d4 = (h0 as u64 * r4 as u64) - + (h1 as u64 * r3 as u64) - + (h2 as u64 * r2 as u64) - + (h3 as u64 * r1 as u64) - + (h4 as u64 * r0 as u64); - - // (partial) h %= p - let mut c: u32; - c = (d0 >> 26) as u32; - h0 = d0 as u32 & 0x3ffffff; - d1 += c as u64; - c = (d1 >> 26) as u32; - h1 = d1 as u32 & 0x3ffffff; - d2 += c as u64; - c = (d2 >> 26) as u32; - h2 = d2 as u32 & 0x3ffffff; - d3 += c as u64; - c = (d3 >> 26) as u32; - h3 = d3 as u32 & 0x3ffffff; - d4 += c as u64; - c = (d4 >> 26) as u32; - h4 = d4 as u32 & 0x3ffffff; - h0 += c * 5; - c = h0 >> 26; - h0 &= 0x3ffffff; - h1 += c; - - self.h[0] = h0; - self.h[1] = h1; - self.h[2] = h2; - self.h[3] = h3; - self.h[4] = h4; - } + impl Poly1305 { + pub fn new(key: &[u8]) -> Poly1305 { + assert!(key.len() == 32); + let mut poly = Poly1305 { + r: [0u32; 5], + h: [0u32; 5], + pad: [0u32; 4], + leftover: 0, + buffer: [0u8; 16], + finalized: false, + }; + + // r &= 0xffffffc0ffffffc0ffffffc0fffffff + poly.r[0] = (u32::from_le_bytes(key[0..4].try_into().expect("len is 4"))) & 0x3ffffff; + poly.r[1] = + (u32::from_le_bytes(key[3..7].try_into().expect("len is 4")) >> 2) & 0x3ffff03; + poly.r[2] = + (u32::from_le_bytes(key[6..10].try_into().expect("len is 4")) >> 4) & 0x3ffc0ff; + poly.r[3] = + (u32::from_le_bytes(key[9..13].try_into().expect("len is 4")) >> 6) & 0x3f03fff; + poly.r[4] = + (u32::from_le_bytes(key[12..16].try_into().expect("len is 4")) >> 8) & 0x00fffff; + + poly.pad[0] = u32::from_le_bytes(key[16..20].try_into().expect("len is 4")); + poly.pad[1] = u32::from_le_bytes(key[20..24].try_into().expect("len is 4")); + poly.pad[2] = u32::from_le_bytes(key[24..28].try_into().expect("len is 4")); + poly.pad[3] = u32::from_le_bytes(key[28..32].try_into().expect("len is 4")); + + poly + } + + fn block(&mut self, m: &[u8]) { + let hibit: u32 = if self.finalized { 0 } else { 1 << 24 }; + + let r0 = self.r[0]; + let r1 = self.r[1]; + let r2 = self.r[2]; + let r3 = self.r[3]; + let r4 = self.r[4]; + + let s1 = r1 * 5; + let s2 = r2 * 5; + let s3 = r3 * 5; + let s4 = r4 * 5; + + let mut h0 = self.h[0]; + let mut h1 = self.h[1]; + let mut h2 = self.h[2]; + let mut h3 = self.h[3]; + let mut h4 = self.h[4]; + + // h += m + h0 += (u32::from_le_bytes(m[0..4].try_into().expect("len is 4"))) & 0x3ffffff; + h1 += (u32::from_le_bytes(m[3..7].try_into().expect("len is 4")) >> 2) & 0x3ffffff; + h2 += (u32::from_le_bytes(m[6..10].try_into().expect("len is 4")) >> 4) & 0x3ffffff; + h3 += (u32::from_le_bytes(m[9..13].try_into().expect("len is 4")) >> 6) & 0x3ffffff; + h4 += (u32::from_le_bytes(m[12..16].try_into().expect("len is 4")) >> 8) | hibit; + + // h *= r + let d0 = (h0 as u64 * r0 as u64) + + (h1 as u64 * s4 as u64) + + (h2 as u64 * s3 as u64) + + (h3 as u64 * s2 as u64) + + (h4 as u64 * s1 as u64); + let mut d1 = (h0 as u64 * r1 as u64) + + (h1 as u64 * r0 as u64) + + (h2 as u64 * s4 as u64) + + (h3 as u64 * s3 as u64) + + (h4 as u64 * s2 as u64); + let mut d2 = (h0 as u64 * r2 as u64) + + (h1 as u64 * r1 as u64) + + (h2 as u64 * r0 as u64) + + (h3 as u64 * s4 as u64) + + (h4 as u64 * s3 as u64); + let mut d3 = (h0 as u64 * r3 as u64) + + (h1 as u64 * r2 as u64) + + (h2 as u64 * r1 as u64) + + (h3 as u64 * r0 as u64) + + (h4 as u64 * s4 as u64); + let mut d4 = (h0 as u64 * r4 as u64) + + (h1 as u64 * r3 as u64) + + (h2 as u64 * r2 as u64) + + (h3 as u64 * r1 as u64) + + (h4 as u64 * r0 as u64); + + // (partial) h %= p + let mut c: u32; + c = (d0 >> 26) as u32; + h0 = d0 as u32 & 0x3ffffff; + d1 += c as u64; + c = (d1 >> 26) as u32; + h1 = d1 as u32 & 0x3ffffff; + d2 += c as u64; + c = (d2 >> 26) as u32; + h2 = d2 as u32 & 0x3ffffff; + d3 += c as u64; + c = (d3 >> 26) as u32; + h3 = d3 as u32 & 0x3ffffff; + d4 += c as u64; + c = (d4 >> 26) as u32; + h4 = d4 as u32 & 0x3ffffff; + h0 += c * 5; + c = h0 >> 26; + h0 &= 0x3ffffff; + h1 += c; + + self.h[0] = h0; + self.h[1] = h1; + self.h[2] = h2; + self.h[3] = h3; + self.h[4] = h4; + } - pub fn finish(&mut self) { - if self.leftover > 0 { - self.buffer[self.leftover] = 1; - for i in self.leftover + 1..16 { - self.buffer[i] = 0; + pub fn finish(&mut self) { + if self.leftover > 0 { + self.buffer[self.leftover] = 1; + for i in self.leftover + 1..16 { + self.buffer[i] = 0; + } + self.finalized = true; + let tmp = self.buffer; + self.block(&tmp); } - self.finalized = true; - let tmp = self.buffer; - self.block(&tmp); + + // fully carry h + let mut h0 = self.h[0]; + let mut h1 = self.h[1]; + let mut h2 = self.h[2]; + let mut h3 = self.h[3]; + let mut h4 = self.h[4]; + + let mut c: u32; + c = h1 >> 26; + h1 &= 0x3ffffff; + h2 += c; + c = h2 >> 26; + h2 &= 0x3ffffff; + h3 += c; + c = h3 >> 26; + h3 &= 0x3ffffff; + h4 += c; + c = h4 >> 26; + h4 &= 0x3ffffff; + h0 += c * 5; + c = h0 >> 26; + h0 &= 0x3ffffff; + h1 += c; + + // compute h + -p + let mut g0 = h0.wrapping_add(5); + c = g0 >> 26; + g0 &= 0x3ffffff; + let mut g1 = h1.wrapping_add(c); + c = g1 >> 26; + g1 &= 0x3ffffff; + let mut g2 = h2.wrapping_add(c); + c = g2 >> 26; + g2 &= 0x3ffffff; + let mut g3 = h3.wrapping_add(c); + c = g3 >> 26; + g3 &= 0x3ffffff; + let mut g4 = h4.wrapping_add(c).wrapping_sub(1 << 26); + + // select h if h < p, or h + -p if h >= p + let mut mask = (g4 >> (32 - 1)).wrapping_sub(1); + g0 &= mask; + g1 &= mask; + g2 &= mask; + g3 &= mask; + g4 &= mask; + mask = !mask; + h0 = (h0 & mask) | g0; + h1 = (h1 & mask) | g1; + h2 = (h2 & mask) | g2; + h3 = (h3 & mask) | g3; + h4 = (h4 & mask) | g4; + + // h = h % (2^128) + h0 = ((h0) | (h1 << 26)) & 0xffffffff; + h1 = ((h1 >> 6) | (h2 << 20)) & 0xffffffff; + h2 = ((h2 >> 12) | (h3 << 14)) & 0xffffffff; + h3 = ((h3 >> 18) | (h4 << 8)) & 0xffffffff; + + // h = mac = (h + pad) % (2^128) + let mut f: u64; + f = h0 as u64 + self.pad[0] as u64; + h0 = f as u32; + f = h1 as u64 + self.pad[1] as u64 + (f >> 32); + h1 = f as u32; + f = h2 as u64 + self.pad[2] as u64 + (f >> 32); + h2 = f as u32; + f = h3 as u64 + self.pad[3] as u64 + (f >> 32); + h3 = f as u32; + + self.h[0] = h0; + self.h[1] = h1; + self.h[2] = h2; + self.h[3] = h3; } - // fully carry h - let mut h0 = self.h[0]; - let mut h1 = self.h[1]; - let mut h2 = self.h[2]; - let mut h3 = self.h[3]; - let mut h4 = self.h[4]; - - let mut c: u32; - c = h1 >> 26; - h1 &= 0x3ffffff; - h2 += c; - c = h2 >> 26; - h2 &= 0x3ffffff; - h3 += c; - c = h3 >> 26; - h3 &= 0x3ffffff; - h4 += c; - c = h4 >> 26; - h4 &= 0x3ffffff; - h0 += c * 5; - c = h0 >> 26; - h0 &= 0x3ffffff; - h1 += c; - - // compute h + -p - let mut g0 = h0.wrapping_add(5); - c = g0 >> 26; - g0 &= 0x3ffffff; - let mut g1 = h1.wrapping_add(c); - c = g1 >> 26; - g1 &= 0x3ffffff; - let mut g2 = h2.wrapping_add(c); - c = g2 >> 26; - g2 &= 0x3ffffff; - let mut g3 = h3.wrapping_add(c); - c = g3 >> 26; - g3 &= 0x3ffffff; - let mut g4 = h4.wrapping_add(c).wrapping_sub(1 << 26); - - // select h if h < p, or h + -p if h >= p - let mut mask = (g4 >> (32 - 1)).wrapping_sub(1); - g0 &= mask; - g1 &= mask; - g2 &= mask; - g3 &= mask; - g4 &= mask; - mask = !mask; - h0 = (h0 & mask) | g0; - h1 = (h1 & mask) | g1; - h2 = (h2 & mask) | g2; - h3 = (h3 & mask) | g3; - h4 = (h4 & mask) | g4; - - // h = h % (2^128) - h0 = ((h0) | (h1 << 26)) & 0xffffffff; - h1 = ((h1 >> 6) | (h2 << 20)) & 0xffffffff; - h2 = ((h2 >> 12) | (h3 << 14)) & 0xffffffff; - h3 = ((h3 >> 18) | (h4 << 8)) & 0xffffffff; - - // h = mac = (h + pad) % (2^128) - let mut f: u64; - f = h0 as u64 + self.pad[0] as u64; - h0 = f as u32; - f = h1 as u64 + self.pad[1] as u64 + (f >> 32); - h1 = f as u32; - f = h2 as u64 + self.pad[2] as u64 + (f >> 32); - h2 = f as u32; - f = h3 as u64 + self.pad[3] as u64 + (f >> 32); - h3 = f as u32; - - self.h[0] = h0; - self.h[1] = h1; - self.h[2] = h2; - self.h[3] = h3; - } + pub fn input(&mut self, data: &[u8]) { + assert!(!self.finalized); + let mut m = data; - pub fn input(&mut self, data: &[u8]) { - assert!(!self.finalized); - let mut m = data; + if self.leftover > 0 { + let want = min(16 - self.leftover, m.len()); + for i in 0..want { + self.buffer[self.leftover + i] = m[i]; + } + m = &m[want..]; + self.leftover += want; - if self.leftover > 0 { - let want = min(16 - self.leftover, m.len()); - for i in 0..want { - self.buffer[self.leftover + i] = m[i]; - } - m = &m[want..]; - self.leftover += want; + if self.leftover < 16 { + return; + } - if self.leftover < 16 { - return; + // self.block(self.buffer[..]); + let tmp = self.buffer; + self.block(&tmp); + + self.leftover = 0; } - // self.block(self.buffer[..]); - let tmp = self.buffer; - self.block(&tmp); + while m.len() >= 16 { + self.block(&m[0..16]); + m = &m[16..]; + } - self.leftover = 0; + for i in 0..m.len() { + self.buffer[i] = m[i]; + } + self.leftover = m.len(); } - while m.len() >= 16 { - self.block(&m[0..16]); - m = &m[16..]; + pub fn result(&mut self) -> [u8; 16] { + if !self.finalized { + self.finish(); + } + let mut output = [0; 16]; + output[0..4].copy_from_slice(&self.h[0].to_le_bytes()); + output[4..8].copy_from_slice(&self.h[1].to_le_bytes()); + output[8..12].copy_from_slice(&self.h[2].to_le_bytes()); + output[12..16].copy_from_slice(&self.h[3].to_le_bytes()); + output } + } - for i in 0..m.len() { - self.buffer[i] = m[i]; + #[cfg(test)] + mod test { + use core::iter::repeat; + + use super::Poly1305; + + fn poly1305(key: &[u8], msg: &[u8], mac: &mut [u8; 16]) { + let mut poly = Poly1305::new(key); + poly.input(msg); + *mac = poly.result(); } - self.leftover = m.len(); - } - pub fn result(&mut self) -> [u8; 16] { - if !self.finalized { - self.finish(); + #[test] + fn test_nacl_vector() { + let key = [ + 0xee, 0xa6, 0xa7, 0x25, 0x1c, 0x1e, 0x72, 0x91, 0x6d, 0x11, 0xc2, 0xcb, 0x21, 0x4d, + 0x3c, 0x25, 0x25, 0x39, 0x12, 0x1d, 0x8e, 0x23, 0x4e, 0x65, 0x2d, 0x65, 0x1f, 0xa4, + 0xc8, 0xcf, 0xf8, 0x80, + ]; + + let msg = [ + 0x8e, 0x99, 0x3b, 0x9f, 0x48, 0x68, 0x12, 0x73, 0xc2, 0x96, 0x50, 0xba, 0x32, 0xfc, + 0x76, 0xce, 0x48, 0x33, 0x2e, 0xa7, 0x16, 0x4d, 0x96, 0xa4, 0x47, 0x6f, 0xb8, 0xc5, + 0x31, 0xa1, 0x18, 0x6a, 0xc0, 0xdf, 0xc1, 0x7c, 0x98, 0xdc, 0xe8, 0x7b, 0x4d, 0xa7, + 0xf0, 0x11, 0xec, 0x48, 0xc9, 0x72, 0x71, 0xd2, 0xc2, 0x0f, 0x9b, 0x92, 0x8f, 0xe2, + 0x27, 0x0d, 0x6f, 0xb8, 0x63, 0xd5, 0x17, 0x38, 0xb4, 0x8e, 0xee, 0xe3, 0x14, 0xa7, + 0xcc, 0x8a, 0xb9, 0x32, 0x16, 0x45, 0x48, 0xe5, 0x26, 0xae, 0x90, 0x22, 0x43, 0x68, + 0x51, 0x7a, 0xcf, 0xea, 0xbd, 0x6b, 0xb3, 0x73, 0x2b, 0xc0, 0xe9, 0xda, 0x99, 0x83, + 0x2b, 0x61, 0xca, 0x01, 0xb6, 0xde, 0x56, 0x24, 0x4a, 0x9e, 0x88, 0xd5, 0xf9, 0xb3, + 0x79, 0x73, 0xf6, 0x22, 0xa4, 0x3d, 0x14, 0xa6, 0x59, 0x9b, 0x1f, 0x65, 0x4c, 0xb4, + 0x5a, 0x74, 0xe3, 0x55, 0xa5, + ]; + + let expected = [ + 0xf3, 0xff, 0xc7, 0x70, 0x3f, 0x94, 0x00, 0xe5, 0x2a, 0x7d, 0xfb, 0x4b, 0x3d, 0x33, + 0x05, 0xd9, + ]; + + let mut mac = [0u8; 16]; + poly1305(&key, &msg, &mut mac); + assert_eq!(&mac[..], &expected[..]); + + let mut poly = Poly1305::new(&key); + poly.input(&msg[0..32]); + poly.input(&msg[32..96]); + poly.input(&msg[96..112]); + poly.input(&msg[112..120]); + poly.input(&msg[120..124]); + poly.input(&msg[124..126]); + poly.input(&msg[126..127]); + poly.input(&msg[127..128]); + poly.input(&msg[128..129]); + poly.input(&msg[129..130]); + poly.input(&msg[130..131]); + let mac = poly.result(); + assert_eq!(&mac[..], &expected[..]); } - let mut output = [0; 16]; - output[0..4].copy_from_slice(&self.h[0].to_le_bytes()); - output[4..8].copy_from_slice(&self.h[1].to_le_bytes()); - output[8..12].copy_from_slice(&self.h[2].to_le_bytes()); - output[12..16].copy_from_slice(&self.h[3].to_le_bytes()); - output - } -} -#[cfg(test)] -mod test { - use core::iter::repeat; + #[test] + fn donna_self_test() { + let wrap_key = [ + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + ]; - use super::Poly1305; + let wrap_msg = [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, + ]; - fn poly1305(key: &[u8], msg: &[u8], mac: &mut [u8; 16]) { - let mut poly = Poly1305::new(key); - poly.input(msg); - *mac = poly.result(); - } + let wrap_mac = [ + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + ]; - #[test] - fn test_nacl_vector() { - let key = [ - 0xee, 0xa6, 0xa7, 0x25, 0x1c, 0x1e, 0x72, 0x91, 0x6d, 0x11, 0xc2, 0xcb, 0x21, 0x4d, - 0x3c, 0x25, 0x25, 0x39, 0x12, 0x1d, 0x8e, 0x23, 0x4e, 0x65, 0x2d, 0x65, 0x1f, 0xa4, - 0xc8, 0xcf, 0xf8, 0x80, - ]; - - let msg = [ - 0x8e, 0x99, 0x3b, 0x9f, 0x48, 0x68, 0x12, 0x73, 0xc2, 0x96, 0x50, 0xba, 0x32, 0xfc, - 0x76, 0xce, 0x48, 0x33, 0x2e, 0xa7, 0x16, 0x4d, 0x96, 0xa4, 0x47, 0x6f, 0xb8, 0xc5, - 0x31, 0xa1, 0x18, 0x6a, 0xc0, 0xdf, 0xc1, 0x7c, 0x98, 0xdc, 0xe8, 0x7b, 0x4d, 0xa7, - 0xf0, 0x11, 0xec, 0x48, 0xc9, 0x72, 0x71, 0xd2, 0xc2, 0x0f, 0x9b, 0x92, 0x8f, 0xe2, - 0x27, 0x0d, 0x6f, 0xb8, 0x63, 0xd5, 0x17, 0x38, 0xb4, 0x8e, 0xee, 0xe3, 0x14, 0xa7, - 0xcc, 0x8a, 0xb9, 0x32, 0x16, 0x45, 0x48, 0xe5, 0x26, 0xae, 0x90, 0x22, 0x43, 0x68, - 0x51, 0x7a, 0xcf, 0xea, 0xbd, 0x6b, 0xb3, 0x73, 0x2b, 0xc0, 0xe9, 0xda, 0x99, 0x83, - 0x2b, 0x61, 0xca, 0x01, 0xb6, 0xde, 0x56, 0x24, 0x4a, 0x9e, 0x88, 0xd5, 0xf9, 0xb3, - 0x79, 0x73, 0xf6, 0x22, 0xa4, 0x3d, 0x14, 0xa6, 0x59, 0x9b, 0x1f, 0x65, 0x4c, 0xb4, - 0x5a, 0x74, 0xe3, 0x55, 0xa5, - ]; - - let expected = [ - 0xf3, 0xff, 0xc7, 0x70, 0x3f, 0x94, 0x00, 0xe5, 0x2a, 0x7d, 0xfb, 0x4b, 0x3d, 0x33, - 0x05, 0xd9, - ]; - - let mut mac = [0u8; 16]; - poly1305(&key, &msg, &mut mac); - assert_eq!(&mac[..], &expected[..]); - - let mut poly = Poly1305::new(&key); - poly.input(&msg[0..32]); - poly.input(&msg[32..96]); - poly.input(&msg[96..112]); - poly.input(&msg[112..120]); - poly.input(&msg[120..124]); - poly.input(&msg[124..126]); - poly.input(&msg[126..127]); - poly.input(&msg[127..128]); - poly.input(&msg[128..129]); - poly.input(&msg[129..130]); - poly.input(&msg[130..131]); - let mac = poly.result(); - assert_eq!(&mac[..], &expected[..]); - } + let mut mac = [0u8; 16]; + poly1305(&wrap_key, &wrap_msg, &mut mac); + assert_eq!(&mac[..], &wrap_mac[..]); + + let total_key = [ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0xff, 0xfe, 0xfd, 0xfc, 0xfb, 0xfa, 0xf9, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, + ]; + + let total_mac = [ + 0x64, 0xaf, 0xe2, 0xe8, 0xd6, 0xad, 0x7b, 0xbd, 0xd2, 0x87, 0xf9, 0x7c, 0x44, 0x62, + 0x3d, 0x39, + ]; + + let mut tpoly = Poly1305::new(&total_key); + for i in 0..256 { + let key: Vec = repeat(i as u8).take(32).collect(); + let msg: Vec = repeat(i as u8).take(256).collect(); + let mut mac = [0u8; 16]; + poly1305(&key[..], &msg[0..i], &mut mac); + tpoly.input(&mac); + } + let mac = tpoly.result(); + assert_eq!(&mac[..], &total_mac[..]); + } - #[test] - fn donna_self_test() { - let wrap_key = [ - 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - ]; - - let wrap_msg = [ - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, - ]; - - let wrap_mac = [ - 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, - ]; - - let mut mac = [0u8; 16]; - poly1305(&wrap_key, &wrap_msg, &mut mac); - assert_eq!(&mac[..], &wrap_mac[..]); - - let total_key = [ - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0xff, 0xfe, 0xfd, 0xfc, 0xfb, 0xfa, 0xf9, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, - ]; - - let total_mac = [ - 0x64, 0xaf, 0xe2, 0xe8, 0xd6, 0xad, 0x7b, 0xbd, 0xd2, 0x87, 0xf9, 0x7c, 0x44, 0x62, - 0x3d, 0x39, - ]; - - let mut tpoly = Poly1305::new(&total_key); - for i in 0..256 { - let key: Vec = repeat(i as u8).take(32).collect(); - let msg: Vec = repeat(i as u8).take(256).collect(); + #[test] + fn test_tls_vectors() { + // from http://tools.ietf.org/html/draft-agl-tls-chacha20poly1305-04 + let key = b"this is 32-byte key for Poly1305"; + let msg = [0u8; 32]; + let expected = [ + 0x49, 0xec, 0x78, 0x09, 0x0e, 0x48, 0x1e, 0xc6, 0xc2, 0x6b, 0x33, 0xb9, 0x1c, 0xcc, + 0x03, 0x07, + ]; let mut mac = [0u8; 16]; - poly1305(&key[..], &msg[0..i], &mut mac); - tpoly.input(&mac); + poly1305(key, &msg, &mut mac); + assert_eq!(&mac[..], &expected[..]); + + let msg = b"Hello world!"; + let expected = [ + 0xa6, 0xf7, 0x45, 0x00, 0x8f, 0x81, 0xc9, 0x16, 0xa2, 0x0d, 0xcc, 0x74, 0xee, 0xf2, + 0xb2, 0xf0, + ]; + poly1305(key, msg, &mut mac); + assert_eq!(&mac[..], &expected[..]); } - let mac = tpoly.result(); - assert_eq!(&mac[..], &total_mac[..]); } +} +pub use fuzzy_poly1305::*; + +#[cfg(fuzzing)] +mod fuzzy_poly1305 { + #[derive(Clone, Copy)] + pub struct Poly1305 { + tag: [u8; 16], + finalized: bool, + } + + impl Poly1305 { + pub fn new(key: &[u8]) -> Poly1305 { + assert_eq!(key.len(), 32); + let mut poly = Poly1305 { tag: [0; 16], finalized: false }; + poly.tag.copy_from_slice(&key[..16]); - #[test] - fn test_tls_vectors() { - // from http://tools.ietf.org/html/draft-agl-tls-chacha20poly1305-04 - let key = b"this is 32-byte key for Poly1305"; - let msg = [0u8; 32]; - let expected = [ - 0x49, 0xec, 0x78, 0x09, 0x0e, 0x48, 0x1e, 0xc6, 0xc2, 0x6b, 0x33, 0xb9, 0x1c, 0xcc, - 0x03, 0x07, - ]; - let mut mac = [0u8; 16]; - poly1305(key, &msg, &mut mac); - assert_eq!(&mac[..], &expected[..]); - - let msg = b"Hello world!"; - let expected = [ - 0xa6, 0xf7, 0x45, 0x00, 0x8f, 0x81, 0xc9, 0x16, 0xa2, 0x0d, 0xcc, 0x74, 0xee, 0xf2, - 0xb2, 0xf0, - ]; - poly1305(key, msg, &mut mac); - assert_eq!(&mac[..], &expected[..]); + poly + } + + pub fn finish(&mut self) { + self.finalized = true; + } + + pub fn input(&mut self, _data: &[u8]) { + assert!(!self.finalized); + } + + pub fn result(&mut self) -> [u8; 16] { + if !self.finalized { + self.finish(); + } + self.tag + } } } +pub use fuzzy_poly1305::*; From cd90f576cacc19f4a6331f8fa2a6c5e97f2e7966 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 26 Jun 2025 11:21:51 +0000 Subject: [PATCH 3/8] Add a `ChaChaDualPolyReadAdapter` that allows an optional AAD `ChaChaPolyReadAdapter` decodes an arbitrary object and checks the poly1305 tag. In the coming commits, we'll need a variant of this which allows for an *optional* AAD in the poly1305 tag, accepting either tag as valid, but indicating to the caller whether the AAD was used. We could use the actual AAD setup in poly1305, which puts the AAD first in the MAC (and then pads it out to a multiple of 16 bytes), but since we're gonna check both with and without, its nice to instead put the AAD at the end, enabling us to only calculate most of the hash once before cloning its state and adding the AAD block. We do this by swapping the AAD and the data being MAC'd in the AAD-containing MAC check (but leaving them where they belong for the non-AAD-containing MAC check). We also add a corresponding `chachapoly_encrypt_with_swapped_aad` which allows encrypting with the new MAC format. --- lightning/src/crypto/streams.rs | 130 ++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/lightning/src/crypto/streams.rs b/lightning/src/crypto/streams.rs index 82680131f1d..7f30245325b 100644 --- a/lightning/src/crypto/streams.rs +++ b/lightning/src/crypto/streams.rs @@ -1,5 +1,7 @@ use crate::crypto::chacha20::ChaCha20; use crate::crypto::chacha20poly1305rfc::ChaCha20Poly1305RFC; +use crate::crypto::fixed_time_eq; +use crate::crypto::poly1305::Poly1305; use crate::io::{self, Read, Write}; use crate::ln::msgs::DecodeError; @@ -7,6 +9,8 @@ use crate::util::ser::{ FixedLengthReader, LengthLimitedRead, LengthReadableArgs, Readable, Writeable, Writer, }; +use alloc::vec::Vec; + pub(crate) struct ChaChaReader<'a, R: io::Read> { pub chacha: &'a mut ChaCha20, pub read: R, @@ -49,6 +53,132 @@ impl<'a, T: Writeable> Writeable for ChaChaPolyWriteAdapter<'a, T> { } } +/// Encrypts the provided plaintext with the given key using ChaCha20Poly1305 in the modified +/// with-AAD form used in [`ChaChaDualPolyReadAdapter`]. +pub(crate) fn chachapoly_encrypt_with_swapped_aad( + mut plaintext: Vec, key: [u8; 32], aad: [u8; 32], +) -> Vec { + let mut chacha = ChaCha20::new(&key[..], &[0; 12]); + let mut mac_key = [0u8; 64]; + chacha.process_in_place(&mut mac_key); + + let mut mac = Poly1305::new(&mac_key[..32]); + chacha.process_in_place(&mut plaintext[..]); + mac.input(&plaintext[..]); + + if plaintext.len() % 16 != 0 { + mac.input(&[0; 16][0..16 - (plaintext.len() % 16)]); + } + + mac.input(&aad[..]); + // Note that we don't need to pad the AAD since its a multiple of 16 bytes + + mac.input(&(plaintext.len() as u64).to_le_bytes()); + mac.input(&32u64.to_le_bytes()); + + plaintext.extend_from_slice(&mac.result()); + plaintext +} + +/// Enables the use of the serialization macros for objects that need to be simultaneously decrypted +/// and deserialized. This allows us to avoid an intermediate Vec allocation. +/// +/// This variant of [`ChaChaPolyReadAdapter`] calculates Poly1305 tags twice, once using the given +/// key and once with the given 32-byte AAD appended after the encrypted stream, accepting either +/// being correct as sufficient. +/// +/// Note that we do *not* use the provided AAD as the standard ChaCha20Poly1305 AAD as that would +/// require placing it first and prevent us from avoiding redundant Poly1305 rounds. Instead, the +/// ChaCha20Poly1305 MAC check is tweaked to move the AAD to *after* the the contents being +/// checked, effectively treating the contents as the AAD for the AAD-containing MAC but behaving +/// like classic ChaCha20Poly1305 for the non-AAD-containing MAC. +pub(crate) struct ChaChaDualPolyReadAdapter { + pub readable: R, + pub used_aad: bool, +} + +impl LengthReadableArgs<([u8; 32], [u8; 32])> for ChaChaDualPolyReadAdapter { + // Simultaneously read and decrypt an object from a LengthLimitedRead storing it in + // Self::readable. LengthLimitedRead must be used instead of std::io::Read because we need the + // total length to separate out the tag at the end. + fn read( + r: &mut R, params: ([u8; 32], [u8; 32]), + ) -> Result { + if r.remaining_bytes() < 16 { + return Err(DecodeError::InvalidValue); + } + let (key, aad) = params; + + let mut chacha = ChaCha20::new(&key[..], &[0; 12]); + let mut mac_key = [0u8; 64]; + chacha.process_in_place(&mut mac_key); + + #[cfg(not(fuzzing))] + let mut mac = Poly1305::new(&mac_key[..32]); + #[cfg(fuzzing)] + let mut mac = Poly1305::new(&key); + + let decrypted_len = r.remaining_bytes() - 16; + let s = FixedLengthReader::new(r, decrypted_len); + let mut chacha_stream = + ChaChaDualPolyReader { chacha: &mut chacha, poly: &mut mac, read_len: 0, read: s }; + + let readable: T = Readable::read(&mut chacha_stream)?; + chacha_stream.read.eat_remaining()?; + + let read_len = chacha_stream.read_len; + + if read_len % 16 != 0 { + mac.input(&[0; 16][0..16 - (read_len % 16)]); + } + + let mut mac_aad = mac; + + mac_aad.input(&aad[..]); + // Note that we don't need to pad the AAD since its a multiple of 16 bytes + + // For the AAD-containing MAC, swap the AAD and the read data, effectively. + mac_aad.input(&(read_len as u64).to_le_bytes()); + mac_aad.input(&32u64.to_le_bytes()); + + // For the non-AAD-containing MAC, leave the data and AAD where they belong. + mac.input(&0u64.to_le_bytes()); + mac.input(&(read_len as u64).to_le_bytes()); + + let mut tag = [0 as u8; 16]; + r.read_exact(&mut tag)?; + if fixed_time_eq(&mac.result(), &tag) { + Ok(Self { readable, used_aad: false }) + } else if fixed_time_eq(&mac_aad.result(), &tag) { + Ok(Self { readable, used_aad: true }) + } else { + return Err(DecodeError::InvalidValue); + } + } +} + +struct ChaChaDualPolyReader<'a, R: Read> { + chacha: &'a mut ChaCha20, + poly: &'a mut Poly1305, + read_len: usize, + pub read: R, +} + +impl<'a, R: Read> Read for ChaChaDualPolyReader<'a, R> { + // Decrypt bytes from Self::read into `dest`. + // `ChaCha20Poly1305RFC::finish_and_check_tag` must be called to check the tag after all reads + // complete. + fn read(&mut self, dest: &mut [u8]) -> Result { + let res = self.read.read(dest)?; + if res > 0 { + self.poly.input(&dest[0..res]); + self.chacha.process_in_place(&mut dest[0..res]); + self.read_len += res; + } + Ok(res) + } +} + /// Enables the use of the serialization macros for objects that need to be simultaneously decrypted and /// deserialized. This allows us to avoid an intermediate Vec allocation. pub(crate) struct ChaChaPolyReadAdapter { From fb30e80e4ed5dad896f3ef3b6918f5496a6955e6 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Wed, 11 Jun 2025 00:27:22 +0000 Subject: [PATCH 4/8] Remove unused callback arg from `construct_keys_for_onion_message` --- lightning/src/blinded_path/utils.rs | 8 ++++++-- lightning/src/onion_message/messenger.rs | 7 +------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lightning/src/blinded_path/utils.rs b/lightning/src/blinded_path/utils.rs index b17fa01bbcf..0a968d32c6d 100644 --- a/lightning/src/blinded_path/utils.rs +++ b/lightning/src/blinded_path/utils.rs @@ -113,9 +113,13 @@ pub(crate) fn construct_keys_for_onion_message<'a, T, I, F>( where T: secp256k1::Signing + secp256k1::Verification, I: Iterator, - F: FnMut(PublicKey, SharedSecret, PublicKey, [u8; 32], Option, Option>), + F: FnMut(SharedSecret, PublicKey, [u8; 32], Option, Option>), { - build_keys_helper!(session_priv, secp_ctx, callback); + let mut callback_wrapper = + |_, ss, pk, encrypted_payload_rho, unblinded_hop_data, encrypted_payload| { + callback(ss, pk, encrypted_payload_rho, unblinded_hop_data, encrypted_payload); + }; + build_keys_helper!(session_priv, secp_ctx, callback_wrapper); for pk in unblinded_path { build_keys_in_loop!(pk, false, None); diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 31067fad2ed..02ba15cba82 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -2283,12 +2283,7 @@ fn packet_payloads_and_keys< unblinded_path.into_iter(), destination, session_priv, - |_, - onion_packet_ss, - ephemeral_pubkey, - control_tlvs_ss, - unblinded_pk_opt, - enc_payload_opt| { + |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(( From 55048c85ab1b363220a79f29b278d0b7c8a12d9a Mon Sep 17 00:00:00 2001 From: shaavan Date: Thu, 10 Jul 2025 23:35:29 +0530 Subject: [PATCH 5/8] Refactor: Introduce ReceiveAuthKey In the upcoming commit, we introduce the usage of a new `ReceiveAuthKey` that will be used to authenticate message contexts in the received `BlindedMessagePath`s. --- lightning/src/sign/mod.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs index 2610131d96f..830e5f87be7 100644 --- a/lightning/src/sign/mod.rs +++ b/lightning/src/sign/mod.rs @@ -804,6 +804,15 @@ pub struct PeerStorageKey { pub inner: [u8; 32], } +/// A secret key used to authenticate message contexts in received [`BlindedMessagePath`]s. +/// +/// This key ensures that a node only accepts incoming messages delivered through +/// blinded paths that it constructed itself. +/// +/// [`BlindedMessagePath`]: crate::blinded_path::message::BlindedMessagePath +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct ReceiveAuthKey(pub [u8; 32]); + /// Specifies the recipient of an invoice. /// /// This indicates to [`NodeSigner::sign_invoice`] what node secret key should be used to sign From 2648a15d03d37d3885a8a67be4be294923feb791 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 26 Jun 2025 11:16:19 +0000 Subject: [PATCH 6/8] Prepare to auth blinded path contexts with a secret AAD in the MAC When we receive an onion message, we often want to make sure it was sent through a blinded path we constructed. This protects us from various deanonymization attacks where someone can send a message to every node on the network until they find us, effectively unwrapping the blinded path and identifying its recipient. We generally do so by adding authentication tags to our `MessageContext` variants. Because the contexts themselves are encrypted (and MAC'd) to us, we only have to ensure that they cannot be forged, which is trivially accomplished with a simple nonce and a MAC covering it. This logic has ended up being repeated in nearly all of our onion message handlers, and has gotten quite repetitive. Instead, here, we simply authenticate the blinded path contexts using the MAC that's already there, but tweaking it with an additional secret as the AAD in Poly1305. This prevents forgery as the secret is now required to make the MAC check pass. Ultimately this means that no one can ever build a blinded path which terminates at an LDK node that we'll accept, but over time we've come to recognize this as a useful property, rather than something to fight. Here we finally break from the spec fully in our context encryption (not just the contents thereof). This will save a bit of space in some of our `MessageContext`s, though sadly not in the blinded path we include in `Bolt12Offer`s, so they're generally not in space-sensitive blinded paths. We can apply the same logic in our blinded payment paths as well, but we do not do so here. This commit only adds the required changes to the cryptography, for now it uses a constant key of `[41; 32]`. --- lightning/src/blinded_path/message.rs | 10 +++--- lightning/src/blinded_path/payment.rs | 6 ++-- lightning/src/blinded_path/utils.rs | 40 ++++++++++++++++------- lightning/src/ln/blinded_payment_tests.rs | 26 +++++++++------ lightning/src/onion_message/messenger.rs | 16 +++++++-- lightning/src/onion_message/packet.rs | 34 +++++++++++++------ 6 files changed, 92 insertions(+), 40 deletions(-) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index ba99b283ee4..0ae1952370b 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -26,7 +26,7 @@ use crate::offers::nonce::Nonce; use crate::offers::offer::OfferId; use crate::onion_message::packet::ControlTlvs; use crate::routing::gossip::{NodeId, ReadOnlyNetworkGraph}; -use crate::sign::{EntropySource, NodeSigner, Recipient}; +use crate::sign::{EntropySource, NodeSigner, ReceiveAuthKey, Recipient}; use crate::types::payment::PaymentHash; use crate::util::scid_utils; use crate::util::ser::{FixedLengthReader, LengthReadableArgs, Readable, Writeable, Writer}; @@ -94,6 +94,7 @@ impl BlindedMessagePath { recipient_node_id, context, &blinding_secret, + ReceiveAuthKey([41; 32]), // TODO: Pass this in ) .map_err(|_| ())?, })) @@ -661,18 +662,19 @@ pub(crate) const MESSAGE_PADDING_ROUND_OFF: usize = 100; pub(super) fn blinded_hops( secp_ctx: &Secp256k1, intermediate_nodes: &[MessageForwardNode], recipient_node_id: PublicKey, context: MessageContext, session_priv: &SecretKey, + local_node_receive_key: ReceiveAuthKey, ) -> Result, secp256k1::Error> { let pks = intermediate_nodes .iter() - .map(|node| node.node_id) - .chain(core::iter::once(recipient_node_id)); + .map(|node| (node.node_id, None)) + .chain(core::iter::once((recipient_node_id, Some(local_node_receive_key)))); let is_compact = intermediate_nodes.iter().any(|node| node.short_channel_id.is_some()); let tlvs = pks .clone() .skip(1) // The first node's TLVs contains the next node's pubkey .zip(intermediate_nodes.iter().map(|node| node.short_channel_id)) - .map(|(pubkey, scid)| match scid { + .map(|((pubkey, _), scid)| match scid { Some(scid) => NextMessageHop::ShortChannelId(scid), None => NextMessageHop::NodeId(pubkey), }) diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index 95ad76c3644..96913ef3c62 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -664,8 +664,10 @@ pub(super) fn blinded_hops( secp_ctx: &Secp256k1, intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey, payee_tlvs: ReceiveTlvs, session_priv: &SecretKey, ) -> Result, secp256k1::Error> { - let pks = - intermediate_nodes.iter().map(|node| node.node_id).chain(core::iter::once(payee_node_id)); + let pks = intermediate_nodes + .iter() + .map(|node| (node.node_id, None)) + .chain(core::iter::once((payee_node_id, None))); let tlvs = intermediate_nodes .iter() .map(|node| BlindedPaymentTlvsRef::Forward(&node.tlvs)) diff --git a/lightning/src/blinded_path/utils.rs b/lightning/src/blinded_path/utils.rs index 0a968d32c6d..f9792157ded 100644 --- a/lightning/src/blinded_path/utils.rs +++ b/lightning/src/blinded_path/utils.rs @@ -17,10 +17,12 @@ use bitcoin::secp256k1::{self, PublicKey, Scalar, Secp256k1, SecretKey}; use super::message::BlindedMessagePath; use super::{BlindedHop, BlindedPath}; -use crate::crypto::streams::ChaChaPolyWriteAdapter; +use crate::crypto::chacha20poly1305rfc::ChaCha20Poly1305RFC; +use crate::crypto::streams::chachapoly_encrypt_with_swapped_aad; use crate::io; use crate::ln::onion_utils; use crate::onion_message::messenger::Destination; +use crate::sign::ReceiveAuthKey; use crate::util::ser::{Writeable, Writer}; use core::borrow::Borrow; @@ -105,7 +107,6 @@ macro_rules! build_keys_helper { }; } -#[inline] pub(crate) fn construct_keys_for_onion_message<'a, T, I, F>( secp_ctx: &Secp256k1, unblinded_path: I, destination: Destination, session_priv: &SecretKey, mut callback: F, @@ -137,8 +138,7 @@ where Ok(()) } -#[inline] -pub(super) fn construct_keys_for_blinded_path<'a, T, I, F, H>( +fn construct_keys_for_blinded_path<'a, T, I, F, H>( secp_ctx: &Secp256k1, unblinded_path: I, session_priv: &SecretKey, mut callback: F, ) -> Result<(), secp256k1::Error> where @@ -149,7 +149,8 @@ where { build_keys_helper!(session_priv, secp_ctx, callback); - for pk in unblinded_path { + let mut iter = unblinded_path.peekable(); + while let Some(pk) = iter.next() { build_keys_in_loop!(pk, false, None); } Ok(()) @@ -157,6 +158,7 @@ where struct PublicKeyWithTlvs { pubkey: PublicKey, + hop_recv_key: Option, tlvs: W, } @@ -171,20 +173,26 @@ pub(crate) fn construct_blinded_hops<'a, T, I, W>( ) -> Result, secp256k1::Error> where T: secp256k1::Signing + secp256k1::Verification, - I: Iterator, + I: Iterator), W)>, W: Writeable, { let mut blinded_hops = Vec::with_capacity(unblinded_path.size_hint().0); construct_keys_for_blinded_path( secp_ctx, - unblinded_path.map(|(pubkey, tlvs)| PublicKeyWithTlvs { pubkey, tlvs }), + unblinded_path.map(|((pubkey, hop_recv_key), tlvs)| PublicKeyWithTlvs { + pubkey, + hop_recv_key, + tlvs, + }), session_priv, |blinded_node_id, _, _, encrypted_payload_rho, unblinded_hop_data, _| { + let hop_data = unblinded_hop_data.unwrap(); blinded_hops.push(BlindedHop { blinded_node_id, encrypted_payload: encrypt_payload( - unblinded_hop_data.unwrap().tlvs, + hop_data.tlvs, encrypted_payload_rho, + hop_data.hop_recv_key, ), }); }, @@ -193,9 +201,19 @@ where } /// Encrypt TLV payload to be used as a [`crate::blinded_path::BlindedHop::encrypted_payload`]. -fn encrypt_payload(payload: P, encrypted_tlvs_rho: [u8; 32]) -> Vec { - let write_adapter = ChaChaPolyWriteAdapter::new(encrypted_tlvs_rho, &payload); - write_adapter.encode() +fn encrypt_payload( + payload: P, encrypted_tlvs_rho: [u8; 32], hop_recv_key: Option, +) -> Vec { + let mut payload_data = payload.encode(); + if let Some(hop_recv_key) = hop_recv_key { + chachapoly_encrypt_with_swapped_aad(payload_data, encrypted_tlvs_rho, hop_recv_key.0) + } else { + let mut chacha = ChaCha20Poly1305RFC::new(&encrypted_tlvs_rho, &[0; 12], &[]); + let mut tag = [0; 16]; + chacha.encrypt_full_message_in_place(&mut payload_data, &mut tag); + payload_data.extend_from_slice(&tag); + payload_data + } } /// A data structure used exclusively to pad blinded path payloads, ensuring they are of diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 4a868e0eb76..7a67e0e15f4 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -1556,17 +1556,23 @@ fn route_blinding_spec_test_vector() { let blinding_override = PublicKey::from_secret_key(&secp_ctx, &dave_eve_session_priv); assert_eq!(blinding_override, pubkey_from_hex("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f")); // Can't use the public API here as the encrypted payloads contain unknown TLVs. - let path = [(dave_node_id, WithoutLength(&dave_unblinded_tlvs)), (eve_node_id, WithoutLength(&eve_unblinded_tlvs))]; + let path = [ + ((dave_node_id, None), WithoutLength(&dave_unblinded_tlvs)), + ((eve_node_id, None), WithoutLength(&eve_unblinded_tlvs)), + ]; let mut dave_eve_blinded_hops = blinded_path::utils::construct_blinded_hops( - &secp_ctx, path.into_iter(), &dave_eve_session_priv + &secp_ctx, path.into_iter(), &dave_eve_session_priv, ).unwrap(); // Concatenate an additional Bob -> Carol blinded path to the Eve -> Dave blinded path. let bob_carol_session_priv = secret_from_hex("0202020202020202020202020202020202020202020202020202020202020202"); let bob_blinding_point = PublicKey::from_secret_key(&secp_ctx, &bob_carol_session_priv); - let path = [(bob_node_id, WithoutLength(&bob_unblinded_tlvs)), (carol_node_id, WithoutLength(&carol_unblinded_tlvs))]; + let path = [ + ((bob_node_id, None), WithoutLength(&bob_unblinded_tlvs)), + ((carol_node_id, None), WithoutLength(&carol_unblinded_tlvs)), + ]; let bob_carol_blinded_hops = blinded_path::utils::construct_blinded_hops( - &secp_ctx, path.into_iter(), &bob_carol_session_priv + &secp_ctx, path.into_iter(), &bob_carol_session_priv, ).unwrap(); let mut blinded_hops = bob_carol_blinded_hops; @@ -2026,9 +2032,9 @@ fn do_test_trampoline_single_hop_receive(success: bool) { let payee_tlvs = payee_tlvs.authenticate(nonce, &expanded_key); let carol_unblinded_tlvs = payee_tlvs.encode(); - let path = [(carol_node_id, WithoutLength(&carol_unblinded_tlvs))]; + let path = [((carol_node_id, None), WithoutLength(&carol_unblinded_tlvs))]; blinded_path::utils::construct_blinded_hops( - &secp_ctx, path.into_iter(), &carol_alice_trampoline_session_priv + &secp_ctx, path.into_iter(), &carol_alice_trampoline_session_priv, ).unwrap() } else { let payee_tlvs = blinded_path::payment::TrampolineForwardTlvs { @@ -2047,9 +2053,9 @@ fn do_test_trampoline_single_hop_receive(success: bool) { }; let carol_unblinded_tlvs = payee_tlvs.encode(); - let path = [(carol_node_id, WithoutLength(&carol_unblinded_tlvs))]; + let path = [((carol_node_id, None), WithoutLength(&carol_unblinded_tlvs))]; blinded_path::utils::construct_blinded_hops( - &secp_ctx, path.into_iter(), &carol_alice_trampoline_session_priv + &secp_ctx, path.into_iter(), &carol_alice_trampoline_session_priv, ).unwrap() }; @@ -2249,11 +2255,11 @@ fn test_trampoline_unblinded_receive() { }; let carol_unblinded_tlvs = payee_tlvs.encode(); - let path = [(carol_node_id, WithoutLength(&carol_unblinded_tlvs))]; + let path = [((carol_node_id, None), WithoutLength(&carol_unblinded_tlvs))]; let carol_alice_trampoline_session_priv = secret_from_hex("a0f4b8d7b6c2d0ffdfaf718f76e9decaef4d9fb38a8c4addb95c4007cc3eee03"); let carol_blinding_point = PublicKey::from_secret_key(&secp_ctx, &carol_alice_trampoline_session_priv); let carol_blinded_hops = blinded_path::utils::construct_blinded_hops( - &secp_ctx, path.into_iter(), &carol_alice_trampoline_session_priv + &secp_ctx, path.into_iter(), &carol_alice_trampoline_session_priv, ).unwrap(); let route = Route { diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 02ba15cba82..164e9e60f36 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -40,7 +40,7 @@ use crate::ln::msgs::{ }; use crate::ln::onion_utils; use crate::routing::gossip::{NetworkGraph, NodeId, ReadOnlyNetworkGraph}; -use crate::sign::{EntropySource, NodeSigner, Recipient}; +use crate::sign::{EntropySource, NodeSigner, ReceiveAuthKey, Recipient}; use crate::types::features::{InitFeatures, NodeFeatures}; use crate::util::async_poll::{MultiResultFuturePoller, ResultFuture}; use crate::util::logger::{Logger, WithContext}; @@ -1074,11 +1074,12 @@ where }, } }; + let receiving_context_auth_key = ReceiveAuthKey([41; 32]); // TODO: pass this in let next_hop = onion_utils::decode_next_untagged_hop( onion_decode_ss, &msg.onion_routing_packet.hop_data[..], msg.onion_routing_packet.hmac, - (control_tlvs_ss, custom_handler.deref(), logger.deref()), + (control_tlvs_ss, custom_handler.deref(), receiving_context_auth_key, logger.deref()), ); match next_hop { Ok(( @@ -1086,6 +1087,7 @@ where message, control_tlvs: ReceiveControlTlvs::Unblinded(ReceiveTlvs { context }), reply_path, + control_tlvs_authenticated, }, None, )) => match (message, context) { @@ -1114,6 +1116,8 @@ where Ok(PeeledOnion::DNSResolver(msg, None, reply_path)) }, _ => { + // Hide the "`control_tlvs_authenticated` is unused warning". We'll use it here soon + let _ = control_tlvs_authenticated; log_trace!( logger, "Received message was sent on a blinded path with wrong or missing context." @@ -2334,7 +2338,12 @@ fn packet_payloads_and_keys< if let Some(control_tlvs) = final_control_tlvs { payloads.push(( - Payload::Receive { control_tlvs, reply_path: reply_path.take(), message }, + Payload::Receive { + control_tlvs, + reply_path: reply_path.take(), + message, + control_tlvs_authenticated: false, + }, prev_control_tlvs_ss.unwrap(), )); } else { @@ -2343,6 +2352,7 @@ fn packet_payloads_and_keys< control_tlvs: ReceiveControlTlvs::Unblinded(ReceiveTlvs { context: None }), reply_path: reply_path.take(), message, + control_tlvs_authenticated: false, }, prev_control_tlvs_ss.unwrap(), )); diff --git a/lightning/src/onion_message/packet.rs b/lightning/src/onion_message/packet.rs index 632cbc9c8a3..301473fba6a 100644 --- a/lightning/src/onion_message/packet.rs +++ b/lightning/src/onion_message/packet.rs @@ -18,9 +18,10 @@ use super::dns_resolution::DNSResolverMessage; use super::messenger::CustomOnionMessageHandler; use super::offers::OffersMessage; use crate::blinded_path::message::{BlindedMessagePath, ForwardTlvs, NextMessageHop, ReceiveTlvs}; -use crate::crypto::streams::{ChaChaPolyReadAdapter, ChaChaPolyWriteAdapter}; +use crate::crypto::streams::{ChaChaDualPolyReadAdapter, ChaChaPolyWriteAdapter}; use crate::ln::msgs::DecodeError; use crate::ln::onion_utils; +use crate::sign::ReceiveAuthKey; use crate::util::logger::Logger; use crate::util::ser::{ BigSize, FixedLengthReader, LengthLimitedRead, LengthReadable, LengthReadableArgs, Readable, @@ -112,7 +113,14 @@ pub(super) enum Payload { /// This payload is for an intermediate hop. Forward(ForwardControlTlvs), /// This payload is for the final hop. - Receive { control_tlvs: ReceiveControlTlvs, reply_path: Option, message: T }, + Receive { + /// The [`ReceiveControlTlvs`] were authenticated with the additional key which was + /// provided to [`ReadableArgs::read`]. + control_tlvs_authenticated: bool, + control_tlvs: ReceiveControlTlvs, + reply_path: Option, + message: T, + }, } /// The contents of an [`OnionMessage`] as read from the wire. @@ -223,6 +231,7 @@ impl Writeable for (Payload, [u8; 32]) { control_tlvs: ReceiveControlTlvs::Blinded(encrypted_bytes), reply_path, message, + control_tlvs_authenticated: _, } => { _encode_varint_length_prefixed_tlv!(w, { (2, reply_path, option), @@ -238,6 +247,7 @@ impl Writeable for (Payload, [u8; 32]) { control_tlvs: ReceiveControlTlvs::Unblinded(control_tlvs), reply_path, message, + control_tlvs_authenticated: _, } => { let write_adapter = ChaChaPolyWriteAdapter::new(self.1, &control_tlvs); _encode_varint_length_prefixed_tlv!(w, { @@ -252,22 +262,25 @@ impl Writeable for (Payload, [u8; 32]) { } // Uses the provided secret to simultaneously decode and decrypt the control TLVs and data TLV. -impl ReadableArgs<(SharedSecret, &H, &L)> +impl + ReadableArgs<(SharedSecret, &H, ReceiveAuthKey, &L)> for Payload::CustomMessage>> { - fn read(r: &mut R, args: (SharedSecret, &H, &L)) -> Result { - let (encrypted_tlvs_ss, handler, logger) = args; + fn read( + r: &mut R, args: (SharedSecret, &H, ReceiveAuthKey, &L), + ) -> Result { + let (encrypted_tlvs_ss, handler, receive_tlvs_key, logger) = args; let v: BigSize = Readable::read(r)?; let mut rd = FixedLengthReader::new(r, v.0); let mut reply_path: Option = None; - let mut read_adapter: Option> = None; + let mut read_adapter: Option> = None; let rho = onion_utils::gen_rho_from_shared_secret(&encrypted_tlvs_ss.secret_bytes()); let mut message_type: Option = None; let mut message = None; decode_tlv_stream_with_custom_tlv_decode!(&mut rd, { (2, reply_path, option), - (4, read_adapter, (option: LengthReadableArgs, rho)), + (4, read_adapter, (option: LengthReadableArgs, (rho, receive_tlvs_key.0))), }, |msg_type, msg_reader| { if msg_type < 64 { return Ok(false) } // Don't allow reading more than one data TLV from an onion message. @@ -304,17 +317,18 @@ impl ReadableArgs<(Sh match read_adapter { None => return Err(DecodeError::InvalidValue), - Some(ChaChaPolyReadAdapter { readable: ControlTlvs::Forward(tlvs) }) => { - if message_type.is_some() { + Some(ChaChaDualPolyReadAdapter { readable: ControlTlvs::Forward(tlvs), used_aad }) => { + if used_aad || message_type.is_some() { return Err(DecodeError::InvalidValue); } Ok(Payload::Forward(ForwardControlTlvs::Unblinded(tlvs))) }, - Some(ChaChaPolyReadAdapter { readable: ControlTlvs::Receive(tlvs) }) => { + Some(ChaChaDualPolyReadAdapter { readable: ControlTlvs::Receive(tlvs), used_aad }) => { Ok(Payload::Receive { control_tlvs: ReceiveControlTlvs::Unblinded(tlvs), reply_path, message: message.ok_or(DecodeError::InvalidValue)?, + control_tlvs_authenticated: used_aad, }) }, } From ad79527970508efca95ef7b21b6e2fc6a0660eff Mon Sep 17 00:00:00 2001 From: shaavan Date: Sat, 5 Jul 2025 23:05:27 +0530 Subject: [PATCH 7/8] Replace constant key with `ReceiveAuthKey` This commit replaces the hardcoded key used for authenticating the context in incoming `BlindedMessagePath`s with a dedicated `ReceiveAuthKey`. This makes the authentication mechanism explicit and configurable for the user. Changes include: - Introducing `ReceiveAuthKey` to the `NodeSigner`, used to authenticate the context at the final hop of an incoming blinded path. - Updating `BlindedMessagePath::new` to accept a `ReceiveAuthKey` as a parameter during path construction. --- fuzz/src/chanmon_consistency.rs | 11 +- fuzz/src/full_stack.rs | 11 +- fuzz/src/onion_message.rs | 12 +- lightning-dns-resolver/src/lib.rs | 26 ++- lightning/src/blinded_path/message.rs | 11 +- lightning/src/ln/async_payments_tests.rs | 3 + lightning/src/ln/blinded_payment_tests.rs | 4 +- lightning/src/ln/channelmanager.rs | 4 +- lightning/src/offers/flow.rs | 17 +- .../src/onion_message/functional_tests.rs | 169 ++++++++++++++---- lightning/src/onion_message/messenger.rs | 77 +++++--- lightning/src/sign/mod.rs | 29 +++ lightning/src/util/dyn_signer.rs | 8 +- lightning/src/util/test_utils.rs | 28 ++- 14 files changed, 313 insertions(+), 97 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index 59612636a9e..d4ffb12d0b4 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -63,7 +63,8 @@ use lightning::routing::router::{ InFlightHtlcs, Path, PaymentParameters, Route, RouteHop, RouteParameters, Router, }; use lightning::sign::{ - EntropySource, InMemorySigner, NodeSigner, PeerStorageKey, Recipient, SignerProvider, + EntropySource, InMemorySigner, NodeSigner, PeerStorageKey, ReceiveAuthKey, Recipient, + SignerProvider, }; use lightning::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use lightning::util::config::UserConfig; @@ -142,8 +143,8 @@ impl MessageRouter for FuzzRouter { } fn create_blinded_paths( - &self, _recipient: PublicKey, _context: MessageContext, _peers: Vec, - _secp_ctx: &Secp256k1, + &self, _recipient: PublicKey, _local_node_receive_key: ReceiveAuthKey, + _context: MessageContext, _peers: Vec, _secp_ctx: &Secp256k1, ) -> Result, ()> { unreachable!() } @@ -347,6 +348,10 @@ impl NodeSigner for KeyProvider { PeerStorageKey { inner: [42; 32] } } + fn get_receive_auth_key(&self) -> ReceiveAuthKey { + ReceiveAuthKey([41; 32]) + } + fn sign_bolt12_invoice( &self, _invoice: &UnsignedBolt12Invoice, ) -> Result { diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index f6fa07199fa..1d16b5c4a0f 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -57,7 +57,8 @@ use lightning::routing::router::{ }; use lightning::routing::utxo::UtxoLookup; use lightning::sign::{ - EntropySource, InMemorySigner, NodeSigner, PeerStorageKey, Recipient, SignerProvider, + EntropySource, InMemorySigner, NodeSigner, PeerStorageKey, ReceiveAuthKey, Recipient, + SignerProvider, }; use lightning::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use lightning::util::config::{ChannelConfig, UserConfig}; @@ -173,8 +174,8 @@ impl MessageRouter for FuzzRouter { } fn create_blinded_paths( - &self, _recipient: PublicKey, _context: MessageContext, _peers: Vec, - _secp_ctx: &Secp256k1, + &self, _recipient: PublicKey, _local_node_receive_key: ReceiveAuthKey, + _context: MessageContext, _peers: Vec, _secp_ctx: &Secp256k1, ) -> Result, ()> { unreachable!() } @@ -438,6 +439,10 @@ impl NodeSigner for KeyProvider { fn get_peer_storage_key(&self) -> PeerStorageKey { PeerStorageKey { inner: [42; 32] } } + + fn get_receive_auth_key(&self) -> ReceiveAuthKey { + ReceiveAuthKey([41; 32]) + } } impl SignerProvider for KeyProvider { diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs index 03903abbf6b..85fb5a9f513 100644 --- a/fuzz/src/onion_message.rs +++ b/fuzz/src/onion_message.rs @@ -24,7 +24,9 @@ use lightning::onion_message::messenger::{ }; use lightning::onion_message::offers::{OffersMessage, OffersMessageHandler}; use lightning::onion_message::packet::OnionMessageContents; -use lightning::sign::{EntropySource, NodeSigner, PeerStorageKey, Recipient, SignerProvider}; +use lightning::sign::{ + EntropySource, NodeSigner, PeerStorageKey, ReceiveAuthKey, Recipient, SignerProvider, +}; use lightning::types::features::InitFeatures; use lightning::util::logger::Logger; use lightning::util::ser::{LengthReadable, Writeable, Writer}; @@ -104,8 +106,8 @@ impl MessageRouter for TestMessageRouter { } fn create_blinded_paths( - &self, _recipient: PublicKey, _context: MessageContext, _peers: Vec, - _secp_ctx: &Secp256k1, + &self, _recipient: PublicKey, _local_node_receive_key: ReceiveAuthKey, + _context: MessageContext, _peers: Vec, _secp_ctx: &Secp256k1, ) -> Result, ()> { unreachable!() } @@ -278,6 +280,10 @@ impl NodeSigner for KeyProvider { fn get_peer_storage_key(&self) -> PeerStorageKey { unreachable!() } + + fn get_receive_auth_key(&self) -> ReceiveAuthKey { + ReceiveAuthKey([41; 32]) + } } impl SignerProvider for KeyProvider { diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs index 73dccdadd23..68f25ada2d7 100644 --- a/lightning-dns-resolver/src/lib.rs +++ b/lightning-dns-resolver/src/lib.rs @@ -174,7 +174,7 @@ mod test { AOnionMessenger, Destination, MessageRouter, OnionMessagePath, OnionMessenger, }; use lightning::routing::router::RouteParametersConfig; - use lightning::sign::{KeysManager, NodeSigner, Recipient}; + use lightning::sign::{KeysManager, NodeSigner, ReceiveAuthKey, Recipient}; use lightning::types::features::InitFeatures; use lightning::types::payment::PaymentHash; use lightning::util::logger::Logger; @@ -230,11 +230,18 @@ mod test { } fn create_blinded_paths( - &self, recipient: PublicKey, context: MessageContext, _peers: Vec, - secp_ctx: &Secp256k1, + &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, + context: MessageContext, _peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { let keys = KeysManager::new(&[0; 32], 42, 43); - Ok(vec![BlindedMessagePath::one_hop(recipient, context, &keys, secp_ctx).unwrap()]) + Ok(vec![BlindedMessagePath::one_hop( + recipient, + local_node_receive_key, + context, + &keys, + secp_ctx, + ) + .unwrap()]) } } impl Deref for DirectlyConnectedRouter { @@ -336,8 +343,15 @@ mod test { let (msg, context) = payer.resolver.resolve_name(payment_id, name.clone(), &*payer_keys).unwrap(); let query_context = MessageContext::DNSResolver(context); - let reply_path = - BlindedMessagePath::one_hop(payer_id, query_context, &*payer_keys, &secp_ctx).unwrap(); + let receive_key = payer_keys.get_receive_auth_key(); + let reply_path = BlindedMessagePath::one_hop( + payer_id, + receive_key, + query_context, + &*payer_keys, + &secp_ctx, + ) + .unwrap(); payer.pending_messages.lock().unwrap().push(( DNSResolverMessage::DNSSECQuery(msg), MessageSendInstructions::WithSpecifiedReplyPath { diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 0ae1952370b..4796427c2c6 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -57,13 +57,13 @@ impl Readable for BlindedMessagePath { impl BlindedMessagePath { /// Create a one-hop blinded path for a message. pub fn one_hop( - recipient_node_id: PublicKey, context: MessageContext, entropy_source: ES, - secp_ctx: &Secp256k1, + recipient_node_id: PublicKey, local_node_receive_key: ReceiveAuthKey, + context: MessageContext, entropy_source: ES, secp_ctx: &Secp256k1, ) -> Result where ES::Target: EntropySource, { - Self::new(&[], recipient_node_id, context, entropy_source, secp_ctx) + Self::new(&[], recipient_node_id, local_node_receive_key, context, entropy_source, secp_ctx) } /// Create a path for an onion message, to be forwarded along `node_pks`. The last node @@ -73,7 +73,8 @@ impl BlindedMessagePath { // TODO: make all payloads the same size with padding + add dummy hops pub fn new( intermediate_nodes: &[MessageForwardNode], recipient_node_id: PublicKey, - context: MessageContext, entropy_source: ES, secp_ctx: &Secp256k1, + local_node_receive_key: ReceiveAuthKey, context: MessageContext, entropy_source: ES, + secp_ctx: &Secp256k1, ) -> Result where ES::Target: EntropySource, @@ -94,7 +95,7 @@ impl BlindedMessagePath { recipient_node_id, context, &blinding_secret, - ReceiveAuthKey([41; 32]), // TODO: Pass this in + local_node_receive_key, ) .map_err(|_| ())?, })) diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index ab9f172ce1b..3c89837f4f2 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -247,6 +247,7 @@ fn create_static_invoice( .message_router .create_blinded_paths( always_online_counterparty.node.get_our_node_id(), + always_online_counterparty.keys_manager.get_receive_auth_key(), MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]) }), Vec::new(), &secp_ctx, @@ -383,6 +384,7 @@ fn static_invoice_unknown_required_features() { .message_router .create_blinded_paths( nodes[1].node.get_our_node_id(), + nodes[1].keys_manager.get_receive_auth_key(), MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]) }), Vec::new(), &secp_ctx, @@ -1023,6 +1025,7 @@ fn invalid_async_receive_with_retry( .message_router .create_blinded_paths( nodes[1].node.get_our_node_id(), + nodes[1].keys_manager.get_receive_auth_key(), MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]) }), Vec::new(), &secp_ctx, diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index 7a67e0e15f4..7c28f4a8cf2 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -36,7 +36,7 @@ use crate::offers::invoice::UnsignedBolt12Invoice; use crate::offers::nonce::Nonce; use crate::prelude::*; use crate::routing::router::{BlindedTail, Path, Payee, PaymentParameters, RouteHop, RouteParameters, TrampolineHop}; -use crate::sign::{NodeSigner, PeerStorageKey, Recipient}; +use crate::sign::{NodeSigner, PeerStorageKey, ReceiveAuthKey, Recipient}; use crate::util::config::UserConfig; use crate::util::ser::{WithoutLength, Writeable}; use crate::util::test_utils; @@ -1638,6 +1638,7 @@ fn route_blinding_spec_test_vector() { &self, _invoice: &RawBolt11Invoice, _recipient: Recipient, ) -> Result { unreachable!() } fn get_peer_storage_key(&self) -> PeerStorageKey { unreachable!() } + fn get_receive_auth_key(&self) -> ReceiveAuthKey { unreachable!() } fn sign_bolt12_invoice( &self, _invoice: &UnsignedBolt12Invoice, ) -> Result { unreachable!() } @@ -1948,6 +1949,7 @@ fn test_trampoline_inbound_payment_decoding() { &self, _invoice: &RawBolt11Invoice, _recipient: Recipient, ) -> Result { unreachable!() } fn get_peer_storage_key(&self) -> PeerStorageKey { unreachable!() } + fn get_receive_auth_key(&self) -> ReceiveAuthKey { unreachable!() } fn sign_bolt12_invoice( &self, _invoice: &UnsignedBolt12Invoice, ) -> Result { unreachable!() } diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 8c034b23343..e1d7ee317cc 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -3750,7 +3750,7 @@ where let flow = OffersMessageFlow::new( ChainHash::using_genesis_block(params.network), params.best_block, our_network_pubkey, current_timestamp, expanded_inbound_key, - secp_ctx.clone(), message_router + node_signer.get_receive_auth_key(), secp_ctx.clone(), message_router ); ChannelManager { @@ -15754,7 +15754,7 @@ where let flow = OffersMessageFlow::new( chain_hash, best_block, our_network_pubkey, highest_seen_timestamp, expanded_inbound_key, - secp_ctx.clone(), args.message_router + args.node_signer.get_receive_auth_key(), secp_ctx.clone(), args.message_router ).with_async_payments_offers_cache(async_receive_offer_cache); let channel_manager = ChannelManager { diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 645724e9af2..dd87d3f6a63 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -54,7 +54,7 @@ use crate::onion_message::messenger::{Destination, MessageRouter, MessageSendIns use crate::onion_message::offers::OffersMessage; use crate::onion_message::packet::OnionMessageContents; use crate::routing::router::Router; -use crate::sign::{EntropySource, NodeSigner}; +use crate::sign::{EntropySource, NodeSigner, ReceiveAuthKey}; use crate::sync::{Mutex, RwLock}; use crate::types::payment::{PaymentHash, PaymentSecret}; use crate::util::ser::Writeable; @@ -95,6 +95,8 @@ where highest_seen_timestamp: AtomicUsize, inbound_payment_key: inbound_payment::ExpandedKey, + receive_auth_key: ReceiveAuthKey, + secp_ctx: Secp256k1, message_router: MR, @@ -123,7 +125,7 @@ where pub fn new( chain_hash: ChainHash, best_block: BestBlock, our_network_pubkey: PublicKey, current_timestamp: u32, inbound_payment_key: inbound_payment::ExpandedKey, - secp_ctx: Secp256k1, message_router: MR, + receive_auth_key: ReceiveAuthKey, secp_ctx: Secp256k1, message_router: MR, ) -> Self { Self { chain_hash, @@ -133,6 +135,8 @@ where highest_seen_timestamp: AtomicUsize::new(current_timestamp as usize), inbound_payment_key, + receive_auth_key, + secp_ctx, message_router, @@ -188,6 +192,10 @@ where self.our_network_pubkey } + fn get_receive_auth_key(&self) -> ReceiveAuthKey { + self.receive_auth_key + } + fn duration_since_epoch(&self) -> Duration { #[cfg(not(feature = "std"))] let now = Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64); @@ -326,11 +334,12 @@ where &self, peers: Vec, context: MessageContext, ) -> Result, ()> { let recipient = self.get_our_node_id(); + let receive_key = self.get_receive_auth_key(); let secp_ctx = &self.secp_ctx; let peers = peers.into_iter().map(|node| node.node_id).collect(); self.message_router - .create_blinded_paths(recipient, context, peers, secp_ctx) + .create_blinded_paths(recipient, receive_key, context, peers, secp_ctx) .and_then(|paths| (!paths.is_empty()).then(|| paths).ok_or(())) } @@ -342,11 +351,13 @@ where &self, peers: Vec, context: OffersContext, ) -> Result, ()> { let recipient = self.get_our_node_id(); + let receive_key = self.get_receive_auth_key(); let secp_ctx = &self.secp_ctx; self.message_router .create_compact_blinded_paths( recipient, + receive_key, MessageContext::Offers(context), peers, secp_ctx, diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index 98fd98d2086..3cbb618dc0b 100644 --- a/lightning/src/onion_message/functional_tests.rs +++ b/lightning/src/onion_message/functional_tests.rs @@ -432,8 +432,10 @@ fn one_blinded_hop() { let secp_ctx = Secp256k1::new(); let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[1].entropy_source; + let receive_key = nodes[1].messenger.node_signer.get_receive_auth_key(); let blinded_path = - BlindedMessagePath::new(&[], nodes[1].node_id, context, entropy, &secp_ctx).unwrap(); + BlindedMessagePath::new(&[], nodes[1].node_id, receive_key, context, entropy, &secp_ctx) + .unwrap(); let destination = Destination::BlindedPath(blinded_path); let instructions = MessageSendInstructions::WithoutReplyPath { destination }; nodes[0].messenger.send_onion_message(test_msg, instructions).unwrap(); @@ -451,9 +453,16 @@ fn two_unblinded_two_blinded() { [MessageForwardNode { node_id: nodes[3].node_id, short_channel_id: None }]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[4].entropy_source; - let blinded_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[4].node_id, context, entropy, &secp_ctx) - .unwrap(); + let receive_key = nodes[4].messenger.node_signer.get_receive_auth_key(); + let blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[4].node_id, + receive_key, + context, + entropy, + &secp_ctx, + ) + .unwrap(); let path = OnionMessagePath { intermediate_nodes: vec![nodes[1].node_id, nodes[2].node_id], destination: Destination::BlindedPath(blinded_path), @@ -477,9 +486,16 @@ fn three_blinded_hops() { ]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[3].entropy_source; - let blinded_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[3].node_id, context, entropy, &secp_ctx) - .unwrap(); + let receive_key = nodes[3].messenger.node_signer.get_receive_auth_key(); + let blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[3].node_id, + receive_key, + context, + entropy, + &secp_ctx, + ) + .unwrap(); let destination = Destination::BlindedPath(blinded_path); let instructions = MessageSendInstructions::WithoutReplyPath { destination }; @@ -504,8 +520,10 @@ fn async_response_over_one_blinded_hop() { let secp_ctx = Secp256k1::new(); let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[1].entropy_source; + let receive_key = nodes[1].messenger.node_signer.get_receive_auth_key(); let reply_path = - BlindedMessagePath::new(&[], nodes[1].node_id, context, entropy, &secp_ctx).unwrap(); + BlindedMessagePath::new(&[], nodes[1].node_id, receive_key, context, entropy, &secp_ctx) + .unwrap(); // 4. Create a responder using the reply path for Alice. let responder = Some(Responder::new(reply_path)); @@ -543,8 +561,10 @@ fn async_response_with_reply_path_succeeds() { // Alice receives a message from Bob with an added reply_path for responding back. let message = TestCustomMessage::Ping; let context = MessageContext::Custom(Vec::new()); + let entropy = &*bob.entropy_source; + let receive_key = bob.messenger.node_signer.get_receive_auth_key(); let reply_path = - BlindedMessagePath::new(&[], bob.node_id, context, &*bob.entropy_source, &secp_ctx) + BlindedMessagePath::new(&[], bob.node_id, receive_key, context, entropy, &secp_ctx) .unwrap(); // Alice asynchronously responds to Bob, expecting a response back from him. @@ -584,8 +604,10 @@ fn async_response_with_reply_path_fails() { // Alice receives a message from Bob with an added reply_path for responding back. let message = TestCustomMessage::Ping; let context = MessageContext::Custom(Vec::new()); + let entropy = &*bob.entropy_source; + let receive_key = bob.messenger.node_signer.get_receive_auth_key(); let reply_path = - BlindedMessagePath::new(&[], bob.node_id, context, &*bob.entropy_source, &secp_ctx) + BlindedMessagePath::new(&[], bob.node_id, receive_key, context, entropy, &secp_ctx) .unwrap(); // Alice tries to asynchronously respond to Bob, but fails because the nodes are unannounced and @@ -634,11 +656,14 @@ fn test_blinded_path_padding_for_full_length_path() { // Update the context to create a larger final receive TLVs, ensuring that // the hop sizes vary before padding. let context = MessageContext::Custom(vec![0u8; 42]); + let entropy = &*nodes[3].entropy_source; + let receive_key = nodes[3].messenger.node_signer.get_receive_auth_key(); let blinded_path = BlindedMessagePath::new( &intermediate_nodes, nodes[3].node_id, + receive_key, context, - &*nodes[3].entropy_source, + entropy, &secp_ctx, ) .unwrap(); @@ -667,11 +692,14 @@ fn test_blinded_path_no_padding_for_compact_path() { // Update the context to create a larger final receive TLVs, ensuring that // the hop sizes vary before padding. let context = MessageContext::Custom(vec![0u8; 42]); + let entropy = &*nodes[3].entropy_source; + let receive_key = nodes[3].messenger.node_signer.get_receive_auth_key(); let blinded_path = BlindedMessagePath::new( &intermediate_nodes, nodes[3].node_id, + receive_key, context, - &*nodes[3].entropy_source, + entropy, &secp_ctx, ) .unwrap(); @@ -693,9 +721,16 @@ fn we_are_intro_node() { ]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[2].entropy_source; - let blinded_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[2].node_id, context, entropy, &secp_ctx) - .unwrap(); + let receive_key = nodes[2].messenger.node_signer.get_receive_auth_key(); + let blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[2].node_id, + receive_key, + context, + entropy, + &secp_ctx, + ) + .unwrap(); let destination = Destination::BlindedPath(blinded_path); let instructions = MessageSendInstructions::WithoutReplyPath { destination }; @@ -708,9 +743,16 @@ fn we_are_intro_node() { [MessageForwardNode { node_id: nodes[0].node_id, short_channel_id: None }]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[1].entropy_source; - let blinded_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[1].node_id, context, entropy, &secp_ctx) - .unwrap(); + let receive_key = nodes[1].messenger.node_signer.get_receive_auth_key(); + let blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[1].node_id, + receive_key, + context, + entropy, + &secp_ctx, + ) + .unwrap(); let destination = Destination::BlindedPath(blinded_path); let instructions = MessageSendInstructions::WithoutReplyPath { destination }; @@ -731,9 +773,16 @@ fn invalid_blinded_path_error() { [MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[2].entropy_source; - let mut blinded_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[2].node_id, context, entropy, &secp_ctx) - .unwrap(); + let receive_key = nodes[2].messenger.node_signer.get_receive_auth_key(); + let mut blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[2].node_id, + receive_key, + context, + entropy, + &secp_ctx, + ) + .unwrap(); blinded_path.clear_blinded_hops(); let destination = Destination::BlindedPath(blinded_path); let instructions = MessageSendInstructions::WithoutReplyPath { destination }; @@ -760,9 +809,16 @@ fn reply_path() { ]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[0].entropy_source; - let reply_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[0].node_id, context, entropy, &secp_ctx) - .unwrap(); + let receive_key = nodes[0].messenger.node_signer.get_receive_auth_key(); + let reply_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[0].node_id, + receive_key, + context, + entropy, + &secp_ctx, + ) + .unwrap(); nodes[0] .messenger .send_onion_message_using_path(path, test_msg.clone(), Some(reply_path)) @@ -781,9 +837,16 @@ fn reply_path() { ]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[3].entropy_source; - let blinded_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[3].node_id, context, entropy, &secp_ctx) - .unwrap(); + let receive_key = nodes[3].messenger.node_signer.get_receive_auth_key(); + let blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[3].node_id, + receive_key, + context, + entropy, + &secp_ctx, + ) + .unwrap(); let destination = Destination::BlindedPath(blinded_path); let intermediate_nodes = [ MessageForwardNode { node_id: nodes[2].node_id, short_channel_id: None }, @@ -791,9 +854,16 @@ fn reply_path() { ]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[0].entropy_source; - let reply_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[0].node_id, context, entropy, &secp_ctx) - .unwrap(); + let receive_key = nodes[0].messenger.node_signer.get_receive_auth_key(); + let reply_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[0].node_id, + receive_key, + context, + entropy, + &secp_ctx, + ) + .unwrap(); let instructions = MessageSendInstructions::WithSpecifiedReplyPath { destination, reply_path }; nodes[0].messenger.send_onion_message(test_msg, instructions).unwrap(); @@ -889,9 +959,16 @@ fn requests_peer_connection_for_buffered_messages() { [MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[0].entropy_source; - let blinded_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[2].node_id, context, entropy, &secp_ctx) - .unwrap(); + let receive_key = nodes[0].messenger.node_signer.get_receive_auth_key(); + let blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[2].node_id, + receive_key, + context, + entropy, + &secp_ctx, + ) + .unwrap(); let destination = Destination::BlindedPath(blinded_path); let instructions = MessageSendInstructions::WithoutReplyPath { destination }; @@ -931,9 +1008,16 @@ fn drops_buffered_messages_waiting_for_peer_connection() { [MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[0].entropy_source; - let blinded_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[2].node_id, context, entropy, &secp_ctx) - .unwrap(); + let receive_key = nodes[0].messenger.node_signer.get_receive_auth_key(); + let blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[2].node_id, + receive_key, + context, + entropy, + &secp_ctx, + ) + .unwrap(); let destination = Destination::BlindedPath(blinded_path); let instructions = MessageSendInstructions::WithoutReplyPath { destination }; @@ -989,9 +1073,16 @@ fn intercept_offline_peer_oms() { [MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: None }]; let context = MessageContext::Custom(Vec::new()); let entropy = &*nodes[2].entropy_source; - let blinded_path = - BlindedMessagePath::new(&intermediate_nodes, nodes[2].node_id, context, entropy, &secp_ctx) - .unwrap(); + let receive_key = nodes[2].messenger.node_signer.get_receive_auth_key(); + let blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[2].node_id, + receive_key, + context, + entropy, + &secp_ctx, + ) + .unwrap(); let destination = Destination::BlindedPath(blinded_path); let instructions = MessageSendInstructions::WithoutReplyPath { destination }; diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 164e9e60f36..96021815d24 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -195,6 +195,7 @@ where /// # use lightning::ln::peer_handler::IgnoringMessageHandler; /// # use lightning::onion_message::messenger::{Destination, MessageRouter, MessageSendInstructions, OnionMessagePath, OnionMessenger}; /// # use lightning::onion_message::packet::OnionMessageContents; +/// # use lightning::sign::{NodeSigner, ReceiveAuthKey}; /// # use lightning::util::logger::{Logger, Record}; /// # use lightning::util::ser::{Writeable, Writer}; /// # use lightning::io; @@ -217,7 +218,8 @@ where /// # }) /// # } /// # fn create_blinded_paths( -/// # &self, _recipient: PublicKey, _context: MessageContext, _peers: Vec, _secp_ctx: &Secp256k1 +/// # &self, _recipient: PublicKey, _local_node_receive_key: ReceiveAuthKey, +/// # _context: MessageContext, _peers: Vec, _secp_ctx: &Secp256k1 /// # ) -> Result, ()> { /// # unreachable!() /// # } @@ -273,7 +275,8 @@ where /// MessageForwardNode { node_id: hop_node_id4, short_channel_id: None }, /// ]; /// let context = MessageContext::Custom(Vec::new()); -/// let blinded_path = BlindedMessagePath::new(&hops, your_node_id, context, &keys_manager, &secp_ctx).unwrap(); +/// let receive_key = keys_manager.get_receive_auth_key(); +/// let blinded_path = BlindedMessagePath::new(&hops, your_node_id, receive_key, context, &keys_manager, &secp_ctx).unwrap(); /// /// // Send a custom onion message to a blinded path. /// let destination = Destination::BlindedPath(blinded_path); @@ -306,6 +309,9 @@ pub struct OnionMessenger< CMH::Target: CustomOnionMessageHandler, { entropy_source: ES, + #[cfg(test)] + pub(super) node_signer: NS, + #[cfg(not(test))] node_signer: NS, logger: L, message_recipients: Mutex>, @@ -501,8 +507,8 @@ pub trait MessageRouter { /// Creates [`BlindedMessagePath`]s to the `recipient` node. The nodes in `peers` are assumed to /// be direct peers with the `recipient`. fn create_blinded_paths( - &self, recipient: PublicKey, context: MessageContext, peers: Vec, - secp_ctx: &Secp256k1, + &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, + context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()>; /// Creates compact [`BlindedMessagePath`]s to the `recipient` node. The nodes in `peers` are @@ -519,14 +525,14 @@ pub trait MessageRouter { /// The provided implementation simply delegates to [`MessageRouter::create_blinded_paths`], /// ignoring the short channel ids. fn create_compact_blinded_paths( - &self, recipient: PublicKey, context: MessageContext, peers: Vec, - secp_ctx: &Secp256k1, + &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, + context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { let peers = peers .into_iter() .map(|MessageForwardNode { node_id, short_channel_id: _ }| node_id) .collect(); - self.create_blinded_paths(recipient, context, peers, secp_ctx) + self.create_blinded_paths(recipient, local_node_receive_key, context, peers, secp_ctx) } } @@ -561,8 +567,9 @@ where I: ExactSizeIterator, T: secp256k1::Signing + secp256k1::Verification, >( - network_graph: &G, recipient: PublicKey, context: MessageContext, peers: I, - entropy_source: &ES, secp_ctx: &Secp256k1, compact_paths: bool, + network_graph: &G, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, + context: MessageContext, peers: I, entropy_source: &ES, secp_ctx: &Secp256k1, + compact_paths: bool, ) -> Result, ()> { // Limit the number of blinded paths that are computed. const MAX_PATHS: usize = 3; @@ -601,7 +608,14 @@ where let paths = peer_info .into_iter() .map(|(peer, _, _)| { - BlindedMessagePath::new(&[peer], recipient, context.clone(), entropy, secp_ctx) + BlindedMessagePath::new( + &[peer], + recipient, + local_node_receive_key, + context.clone(), + entropy, + secp_ctx, + ) }) .take(MAX_PATHS) .collect::, _>>(); @@ -610,8 +624,15 @@ where Ok(paths) if !paths.is_empty() => Ok(paths), _ => { if is_recipient_announced { - BlindedMessagePath::new(&[], recipient, context, &**entropy_source, secp_ctx) - .map(|path| vec![path]) + BlindedMessagePath::new( + &[], + recipient, + local_node_receive_key, + context, + &**entropy_source, + secp_ctx, + ) + .map(|path| vec![path]) } else { Err(()) } @@ -669,14 +690,16 @@ where } pub(crate) fn create_blinded_paths( - network_graph: &G, recipient: PublicKey, context: MessageContext, peers: Vec, - entropy_source: &ES, secp_ctx: &Secp256k1, + network_graph: &G, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, + context: MessageContext, peers: Vec, entropy_source: &ES, + secp_ctx: &Secp256k1, ) -> Result, ()> { let peers = peers.into_iter().map(|node_id| MessageForwardNode { node_id, short_channel_id: None }); Self::create_blinded_paths_from_iter( network_graph, recipient, + local_node_receive_key, context, peers.into_iter(), entropy_source, @@ -686,12 +709,14 @@ where } pub(crate) fn create_compact_blinded_paths( - network_graph: &G, recipient: PublicKey, context: MessageContext, - peers: Vec, entropy_source: &ES, secp_ctx: &Secp256k1, + network_graph: &G, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, + context: MessageContext, peers: Vec, entropy_source: &ES, + secp_ctx: &Secp256k1, ) -> Result, ()> { Self::create_blinded_paths_from_iter( network_graph, recipient, + local_node_receive_key, context, peers.into_iter(), entropy_source, @@ -714,12 +739,13 @@ where } fn create_blinded_paths( - &self, recipient: PublicKey, context: MessageContext, peers: Vec, - secp_ctx: &Secp256k1, + &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, + context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { Self::create_blinded_paths( &self.network_graph, recipient, + local_node_receive_key, context, peers, &self.entropy_source, @@ -728,12 +754,13 @@ where } fn create_compact_blinded_paths( - &self, recipient: PublicKey, context: MessageContext, peers: Vec, - secp_ctx: &Secp256k1, + &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, + context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { Self::create_compact_blinded_paths( &self.network_graph, recipient, + local_node_receive_key, context, peers, &self.entropy_source, @@ -1074,7 +1101,7 @@ where }, } }; - let receiving_context_auth_key = ReceiveAuthKey([41; 32]); // TODO: pass this in + let receiving_context_auth_key = node_signer.get_receive_auth_key(); let next_hop = onion_utils::decode_next_untagged_hop( onion_decode_ss, &msg.onion_routing_packet.hop_data[..], @@ -1440,7 +1467,13 @@ where }; self.message_router - .create_blinded_paths(recipient, context, peers, secp_ctx) + .create_blinded_paths( + recipient, + self.node_signer.get_receive_auth_key(), + context, + peers, + secp_ctx, + ) .and_then(|paths| paths.into_iter().next().ok_or(())) .map_err(|_| SendError::PathNotFound) } diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs index 830e5f87be7..a1ba428ded6 100644 --- a/lightning/src/sign/mod.rs +++ b/lightning/src/sign/mod.rs @@ -860,6 +860,19 @@ pub trait NodeSigner { /// can be re-derived from data which would be available after state loss (eg the wallet seed). fn get_peer_storage_key(&self) -> PeerStorageKey; + /// Returns the [`ReceiveAuthKey`] used to authenticate incoming [`BlindedMessagePath`] contexts. + /// + /// This key is used as additional associated data (AAD) during MAC verification of the + /// [`MessageContext`] at the final hop of a blinded path. It ensures that only paths + /// constructed by this node will be accepted, preventing unauthorized parties from forging + /// valid-looking messages. + /// + /// Implementers must ensure that this key remains secret and consistent across invocations. + /// + /// [`BlindedMessagePath`]: crate::blinded_path::message::BlindedMessagePath + /// [`MessageContext`]: crate::blinded_path::message::MessageContext + fn get_receive_auth_key(&self) -> ReceiveAuthKey; + /// Get node id based on the provided [`Recipient`]. /// /// This method must return the same value each time it is called with a given [`Recipient`] @@ -1836,6 +1849,7 @@ pub struct KeysManager { channel_master_key: Xpriv, channel_child_index: AtomicUsize, peer_storage_key: PeerStorageKey, + receive_auth_key: ReceiveAuthKey, #[cfg(test)] pub(crate) entropy_source: RandomBytes, @@ -1873,6 +1887,7 @@ impl KeysManager { const CHANNEL_MASTER_KEY_INDEX: ChildNumber = ChildNumber::Hardened { index: 3 }; const INBOUND_PAYMENT_KEY_INDEX: ChildNumber = ChildNumber::Hardened { index: 5 }; const PEER_STORAGE_KEY_INDEX: ChildNumber = ChildNumber::Hardened { index: 6 }; + const RECEIVE_AUTH_KEY_INDEX: ChildNumber = ChildNumber::Hardened { index: 7 }; let secp_ctx = Secp256k1::new(); // Note that when we aren't serializing the key, network doesn't matter @@ -1915,6 +1930,11 @@ impl KeysManager { .expect("Your RNG is busted") .private_key; + let receive_auth_key = master_key + .derive_priv(&secp_ctx, &RECEIVE_AUTH_KEY_INDEX) + .expect("Your RNG is busted") + .private_key; + let mut rand_bytes_engine = Sha256::engine(); rand_bytes_engine.input(&starting_time_secs.to_be_bytes()); rand_bytes_engine.input(&starting_time_nanos.to_be_bytes()); @@ -1930,6 +1950,7 @@ impl KeysManager { inbound_payment_key: ExpandedKey::new(inbound_pmt_key_bytes), peer_storage_key: PeerStorageKey { inner: peer_storage_key.secret_bytes() }, + receive_auth_key: ReceiveAuthKey(receive_auth_key.secret_bytes()), destination_script, shutdown_pubkey, @@ -2160,6 +2181,10 @@ impl NodeSigner for KeysManager { self.peer_storage_key.clone() } + fn get_receive_auth_key(&self) -> ReceiveAuthKey { + self.receive_auth_key.clone() + } + fn sign_invoice( &self, invoice: &RawBolt11Invoice, recipient: Recipient, ) -> Result { @@ -2325,6 +2350,10 @@ impl NodeSigner for PhantomKeysManager { self.inner.peer_storage_key.clone() } + fn get_receive_auth_key(&self) -> ReceiveAuthKey { + self.inner.receive_auth_key.clone() + } + fn sign_invoice( &self, invoice: &RawBolt11Invoice, recipient: Recipient, ) -> Result { diff --git a/lightning/src/util/dyn_signer.rs b/lightning/src/util/dyn_signer.rs index 7e9844e2ac0..ea062de5c76 100644 --- a/lightning/src/util/dyn_signer.rs +++ b/lightning/src/util/dyn_signer.rs @@ -14,8 +14,8 @@ use crate::ln::script::ShutdownScript; use crate::sign::ecdsa::EcdsaChannelSigner; #[cfg(taproot)] use crate::sign::taproot::TaprootChannelSigner; -use crate::sign::ChannelSigner; use crate::sign::InMemorySigner; +use crate::sign::{ChannelSigner, ReceiveAuthKey}; use crate::sign::{EntropySource, HTLCDescriptor, OutputSpender, PhantomKeysManager}; use crate::sign::{ NodeSigner, PeerStorageKey, Recipient, SignerProvider, SpendableOutputDescriptor, @@ -217,7 +217,8 @@ inner, invoice: &crate::offers::invoice::UnsignedBolt12Invoice ) -> Result, fn get_inbound_payment_key(,) -> ExpandedKey, - fn get_peer_storage_key(,) -> PeerStorageKey + fn get_peer_storage_key(,) -> PeerStorageKey, + fn get_receive_auth_key(,) -> ReceiveAuthKey ); delegate!(DynKeysInterface, SignerProvider, @@ -282,7 +283,8 @@ delegate!(DynPhantomKeysInterface, NodeSigner, fn sign_bolt12_invoice(, invoice: &crate::offers::invoice::UnsignedBolt12Invoice ) -> Result, fn get_inbound_payment_key(,) -> ExpandedKey, - fn get_peer_storage_key(,) -> PeerStorageKey + fn get_peer_storage_key(,) -> PeerStorageKey, + fn get_receive_auth_key(,) -> ReceiveAuthKey ); impl SignerProvider for DynPhantomKeysInterface { diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index e0869bf4364..4165afea767 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -46,7 +46,7 @@ use crate::routing::router::{ }; use crate::routing::scoring::{ChannelUsage, ScoreLookUp, ScoreUpdate}; use crate::routing::utxo::{UtxoLookup, UtxoLookupError, UtxoResult}; -use crate::sign; +use crate::sign::{self, ReceiveAuthKey}; use crate::sign::{ChannelSigner, PeerStorageKey}; use crate::sync::RwLock; use crate::types::features::{ChannelFeatures, InitFeatures, NodeFeatures}; @@ -346,8 +346,8 @@ impl<'a> MessageRouter for TestMessageRouter<'a> { } fn create_blinded_paths( - &self, recipient: PublicKey, context: MessageContext, peers: Vec, - secp_ctx: &Secp256k1, + &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, + context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { let mut peers = peers; { @@ -356,12 +356,12 @@ impl<'a> MessageRouter for TestMessageRouter<'a> { peers = peers_override.clone(); } } - self.inner.create_blinded_paths(recipient, context, peers, secp_ctx) + self.inner.create_blinded_paths(recipient, local_node_receive_key, context, peers, secp_ctx) } fn create_compact_blinded_paths( - &self, recipient: PublicKey, context: MessageContext, peers: Vec, - secp_ctx: &Secp256k1, + &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, + context: MessageContext, peers: Vec, secp_ctx: &Secp256k1, ) -> Result, ()> { let mut peers = peers; { @@ -374,7 +374,13 @@ impl<'a> MessageRouter for TestMessageRouter<'a> { .collect(); } } - self.inner.create_compact_blinded_paths(recipient, context, peers, secp_ctx) + self.inner.create_compact_blinded_paths( + recipient, + local_node_receive_key, + context, + peers, + secp_ctx, + ) } } @@ -1524,6 +1530,10 @@ impl NodeSigner for TestNodeSigner { unreachable!() } + fn get_receive_auth_key(&self) -> ReceiveAuthKey { + ReceiveAuthKey(self.node_secret.secret_bytes()) + } + fn get_node_id(&self, recipient: Recipient) -> Result { let node_secret = match recipient { Recipient::Node => Ok(&self.node_secret), @@ -1608,6 +1618,10 @@ impl NodeSigner for TestKeysInterface { self.backing.get_peer_storage_key() } + fn get_receive_auth_key(&self) -> ReceiveAuthKey { + self.backing.get_receive_auth_key() + } + fn sign_bolt12_invoice( &self, invoice: &UnsignedBolt12Invoice, ) -> Result { From 9ce5b19535a8f83166cdbb3c2ae7ec6db5c09e1e Mon Sep 17 00:00:00 2001 From: shaavan Date: Fri, 11 Jul 2025 00:10:38 +0530 Subject: [PATCH 8/8] Cleanup: Remove redundant (hmac, nonce) from codebase Now that we have introduced an alternate mechanism for authentication in the codebase, we can safely remove the now redundant (hmac, nonce) fields from the MessageContext's while maintaining the security of the onion messages. --- lightning/src/blinded_path/message.rs | 66 ++++---------- lightning/src/ln/channelmanager.rs | 106 ++++------------------ lightning/src/offers/flow.rs | 99 ++++----------------- lightning/src/offers/signer.rs | 123 ++++---------------------- 4 files changed, 64 insertions(+), 330 deletions(-) diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 4796427c2c6..a3d6e8aa9a4 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -30,8 +30,6 @@ use crate::sign::{EntropySource, NodeSigner, ReceiveAuthKey, Recipient}; use crate::types::payment::PaymentHash; use crate::util::scid_utils; use crate::util::ser::{FixedLengthReader, LengthReadableArgs, Readable, Writeable, Writer}; -use bitcoin::hashes::hmac::Hmac; -use bitcoin::hashes::sha256::Hash as Sha256; use core::mem; use core::ops::Deref; @@ -406,12 +404,6 @@ pub enum OffersContext { /// [`Refund`]: crate::offers::refund::Refund /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest nonce: Nonce, - - /// Authentication code for the [`PaymentId`], which should be checked when the context is - /// used with an [`InvoiceError`]. - /// - /// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError - hmac: Option>, }, /// Context used by a [`BlindedMessagePath`] as a reply path for a [`Bolt12Invoice`]. /// @@ -424,19 +416,6 @@ pub enum OffersContext { /// /// [`Bolt12Invoice::payment_hash`]: crate::offers::invoice::Bolt12Invoice::payment_hash payment_hash: PaymentHash, - - /// A nonce used for authenticating that a received [`InvoiceError`] is for a valid - /// sent [`Bolt12Invoice`]. - /// - /// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError - /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice - nonce: Nonce, - - /// Authentication code for the [`PaymentHash`], which should be checked when the context is - /// used to log the received [`InvoiceError`]. - /// - /// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError - hmac: Hmac, }, } @@ -544,35 +523,12 @@ pub enum AsyncPaymentsContext { /// /// [`Offer`]: crate::offers::offer::Offer payment_id: PaymentId, - /// A nonce used for authenticating that a [`ReleaseHeldHtlc`] message is valid for a preceding - /// [`HeldHtlcAvailable`] message. - /// - /// [`ReleaseHeldHtlc`]: crate::onion_message::async_payments::ReleaseHeldHtlc - /// [`HeldHtlcAvailable`]: crate::onion_message::async_payments::HeldHtlcAvailable - nonce: Nonce, - /// Authentication code for the [`PaymentId`]. - /// - /// Prevents the recipient from being able to deanonymize us by creating a blinded path to us - /// containing the expected [`PaymentId`]. - hmac: Hmac, }, /// Context contained within the [`BlindedMessagePath`]s we put in static invoices, provided back /// to us in corresponding [`HeldHtlcAvailable`] messages. /// /// [`HeldHtlcAvailable`]: crate::onion_message::async_payments::HeldHtlcAvailable InboundPayment { - /// A nonce used for authenticating that a [`HeldHtlcAvailable`] message is valid for a - /// preceding static invoice. - /// - /// [`HeldHtlcAvailable`]: crate::onion_message::async_payments::HeldHtlcAvailable - nonce: Nonce, - /// Authentication code for the [`HeldHtlcAvailable`] message. - /// - /// Prevents nodes from creating their own blinded path to us, sending a [`HeldHtlcAvailable`] - /// message and trivially getting notified whenever we come online. - /// - /// [`HeldHtlcAvailable`]: crate::onion_message::async_payments::HeldHtlcAvailable - hmac: Hmac, /// The time as duration since the Unix epoch at which this path expires and messages sent over /// it should be ignored. Without this, anyone with the path corresponding to this context is /// able to trivially ask if we're online forever. @@ -587,6 +543,14 @@ impl_writeable_tlv_based_enum!(MessageContext, {3, DNSResolver} => (), ); +// NOTE: +// Several TLV fields (`nonce`, `hmac`, etc.) were removed in LDK v0.1.X +// following the introduction of `ReceiveAuthKey`-based authentication for +// inbound `BlindedMessagePath`s. These fields are now commented out and +// their `type` values must not be reused unless support for LDK v0.1.X +// and earlier is fully dropped. +// +// For context-specific removals, see the commented-out fields within each enum variant. impl_writeable_tlv_based_enum!(OffersContext, (0, InvoiceRequest) => { (0, nonce, required), @@ -594,12 +558,12 @@ impl_writeable_tlv_based_enum!(OffersContext, (1, OutboundPayment) => { (0, payment_id, required), (1, nonce, required), - (2, hmac, option), + // Removed: (2, hmac, option) }, (2, InboundPayment) => { (0, payment_hash, required), - (1, nonce, required), - (2, hmac, required) + // Removed: (1, nonce, required), + // Removed: (2, hmac, required) }, (3, StaticInvoiceRequested) => { (0, recipient_id, required), @@ -611,12 +575,12 @@ impl_writeable_tlv_based_enum!(OffersContext, impl_writeable_tlv_based_enum!(AsyncPaymentsContext, (0, OutboundPayment) => { (0, payment_id, required), - (2, nonce, required), - (4, hmac, required), + // Removed: (2, nonce, required), + // Removed: (4, hmac, required), }, (1, InboundPayment) => { - (0, nonce, required), - (2, hmac, required), + // Removed: (0, nonce, required), + // Removed: (2, hmac, required), (4, path_absolute_expiry, required), }, (2, OfferPaths) => { diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index e1d7ee317cc..91949fd6fae 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -545,24 +545,6 @@ pub trait Verification { ) -> Result<(), ()>; } -impl Verification for PaymentHash { - /// Constructs an HMAC to include in [`OffersContext::InboundPayment`] for the payment hash - /// along with the given [`Nonce`]. - fn hmac_for_offer_payment( - &self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, - ) -> Hmac { - signer::hmac_for_payment_hash(*self, nonce, expanded_key) - } - - /// Authenticates the payment id using an HMAC and a [`Nonce`] taken from an - /// [`OffersContext::InboundPayment`]. - fn verify_for_offer_payment( - &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, - ) -> Result<(), ()> { - signer::verify_payment_hash(*self, hmac, nonce, expanded_key) - } -} - impl Verification for UnauthenticatedReceiveTlvs { fn hmac_for_offer_payment( &self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, @@ -587,42 +569,6 @@ pub struct PaymentId(pub [u8; Self::LENGTH]); impl PaymentId { /// Number of bytes in the id. pub const LENGTH: usize = 32; - - /// Constructs an HMAC to include in [`AsyncPaymentsContext::OutboundPayment`] for the payment id - /// along with the given [`Nonce`]. - #[cfg(async_payments)] - pub fn hmac_for_async_payment( - &self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, - ) -> Hmac { - signer::hmac_for_async_payment_id(*self, nonce, expanded_key) - } - - /// Authenticates the payment id using an HMAC and a [`Nonce`] taken from an - /// [`AsyncPaymentsContext::OutboundPayment`]. - #[cfg(async_payments)] - pub fn verify_for_async_payment( - &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, - ) -> Result<(), ()> { - signer::verify_async_payment_id(*self, hmac, nonce, expanded_key) - } -} - -impl Verification for PaymentId { - /// Constructs an HMAC to include in [`OffersContext::OutboundPayment`] for the payment id - /// along with the given [`Nonce`]. - fn hmac_for_offer_payment( - &self, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, - ) -> Hmac { - signer::hmac_for_offer_payment_id(*self, nonce, expanded_key) - } - - /// Authenticates the payment id using an HMAC and a [`Nonce`] taken from an - /// [`OffersContext::OutboundPayment`]. - fn verify_for_offer_payment( - &self, hmac: Hmac, nonce: Nonce, expanded_key: &inbound_payment::ExpandedKey, - ) -> Result<(), ()> { - signer::verify_offer_payment_id(*self, hmac, nonce, expanded_key) - } } impl PaymentId { @@ -5322,10 +5268,7 @@ where }, }; - let entropy = &*self.entropy_source; - let enqueue_held_htlc_available_res = self.flow.enqueue_held_htlc_available( - entropy, invoice, payment_id, self.get_peers_for_blinded_path(), @@ -11317,7 +11260,7 @@ where let invoice = builder.allow_mpp().build_and_sign(secp_ctx)?; - self.flow.enqueue_invoice(entropy, invoice.clone(), refund, self.get_peers_for_blinded_path())?; + self.flow.enqueue_invoice(invoice.clone(), refund, self.get_peers_for_blinded_path())?; Ok(invoice) }, @@ -13324,8 +13267,6 @@ where fn handle_message( &self, message: OffersMessage, context: Option, responder: Option, ) -> Option<(OffersMessage, ResponseInstruction)> { - let expanded_key = &self.inbound_payment_key; - macro_rules! handle_pay_invoice_res { ($res: expr, $invoice: expr, $logger: expr) => {{ let error = match $res { @@ -13441,12 +13382,7 @@ where #[cfg(async_payments)] OffersMessage::StaticInvoice(invoice) => { let payment_id = match context { - Some(OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) }) => { - if payment_id.verify_for_offer_payment(hmac, nonce, expanded_key).is_err() { - return None - } - payment_id - }, + Some(OffersContext::OutboundPayment { payment_id, .. }) => payment_id, _ => return None }; let res = self.initiate_async_payment(&invoice, payment_id); @@ -13454,12 +13390,7 @@ where }, OffersMessage::InvoiceError(invoice_error) => { let payment_hash = match context { - Some(OffersContext::InboundPayment { payment_hash, nonce, hmac }) => { - match payment_hash.verify_for_offer_payment(hmac, nonce, expanded_key) { - Ok(_) => Some(payment_hash), - Err(_) => None, - } - }, + Some(OffersContext::InboundPayment { payment_hash }) => Some(payment_hash), _ => None, }; @@ -13467,12 +13398,10 @@ where log_trace!(logger, "Received invoice_error: {}", invoice_error); match context { - Some(OffersContext::OutboundPayment { payment_id, nonce, hmac: Some(hmac) }) => { - if let Ok(()) = payment_id.verify_for_offer_payment(hmac, nonce, expanded_key) { - self.abandon_payment_with_reason( - payment_id, PaymentFailureReason::InvoiceRequestRejected, - ); - } + Some(OffersContext::OutboundPayment { payment_id, .. }) => { + self.abandon_payment_with_reason( + payment_id, PaymentFailureReason::InvoiceRequestRejected, + ); }, _ => {}, } @@ -13621,15 +13550,18 @@ where fn handle_release_held_htlc(&self, _message: ReleaseHeldHtlc, _context: AsyncPaymentsContext) { #[cfg(async_payments)] { - if let Ok(payment_id) = self.flow.verify_outbound_async_payment_context(_context) { - if let Err(e) = self.send_payment_for_static_invoice(payment_id) { - log_trace!( - self.logger, - "Failed to release held HTLC with payment id {}: {:?}", - payment_id, - e - ); - } + let payment_id = match _context { + AsyncPaymentsContext::OutboundPayment { payment_id } => payment_id, + _ => return, + }; + + if let Err(e) = self.send_payment_for_static_invoice(payment_id) { + log_trace!( + self.logger, + "Failed to release held HTLC with payment id {}: {:?}", + payment_id, + e + ); } } } diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index dd87d3f6a63..a1b83260dfd 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -32,9 +32,7 @@ use crate::prelude::*; use crate::chain::BestBlock; use crate::ln::channel_state::ChannelDetails; -use crate::ln::channelmanager::{ - Verification, {PaymentId, CLTV_FAR_FAR_AWAY, MAX_SHORT_LIVED_RELATIVE_EXPIRY}, -}; +use crate::ln::channelmanager::{PaymentId, CLTV_FAR_FAR_AWAY, MAX_SHORT_LIVED_RELATIVE_EXPIRY}; use crate::ln::inbound_payment; use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache; use crate::offers::invoice::{ @@ -64,7 +62,6 @@ use { crate::blinded_path::message::AsyncPaymentsContext, crate::blinded_path::payment::AsyncBolt12OfferContext, crate::offers::offer::Amount, - crate::offers::signer, crate::offers::static_invoice::{StaticInvoice, StaticInvoiceBuilder}, crate::onion_message::async_payments::{ HeldHtlcAvailable, OfferPaths, OfferPathsRequest, ServeStaticInvoice, @@ -559,9 +556,7 @@ where &self, context: AsyncPaymentsContext, ) -> Result<(), ()> { match context { - AsyncPaymentsContext::InboundPayment { nonce, hmac, path_absolute_expiry } => { - signer::verify_held_htlc_available_context(nonce, hmac, &self.inbound_payment_key)?; - + AsyncPaymentsContext::InboundPayment { path_absolute_expiry } => { if self.duration_since_epoch() > path_absolute_expiry { return Err(()); } @@ -571,30 +566,6 @@ where } } - /// Verifies the provided [`AsyncPaymentsContext`] for an inbound [`ReleaseHeldHtlc`] message. - /// - /// The context is verified using the `nonce` and `hmac` values, and if valid, - /// returns the associated [`PaymentId`]. - /// - /// # Errors - /// - /// Returns `Err(())` if: - /// - The HMAC verification fails for outbound context. - /// - /// [`ReleaseHeldHtlc`]: crate::onion_message::async_payments::ReleaseHeldHtlc - #[cfg(async_payments)] - pub fn verify_outbound_async_payment_context( - &self, context: AsyncPaymentsContext, - ) -> Result { - match context { - AsyncPaymentsContext::OutboundPayment { payment_id, hmac, nonce } => { - payment_id.verify_for_async_payment(hmac, nonce, &self.inbound_payment_key)?; - Ok(payment_id) - }, - _ => Err(()), - } - } - /// Creates an [`OfferBuilder`] such that the [`Offer`] it builds is recognized by the /// [`OffersMessageFlow`], and any corresponding [`InvoiceRequest`] can be verified using /// [`Self::verify_invoice_request`]. The offer will expire at `absolute_expiry` if `Some`, @@ -734,7 +705,7 @@ where let secp_ctx = &self.secp_ctx; let nonce = Nonce::from_entropy_source(entropy); - let context = OffersContext::OutboundPayment { payment_id, nonce, hmac: None }; + let context = OffersContext::OutboundPayment { payment_id, nonce }; let path = self .create_blinded_paths_using_absolute_expiry(context, Some(absolute_expiry), peers) @@ -814,16 +785,12 @@ where ) .map_err(|()| Bolt12SemanticError::MissingPaths)?; - let nonce = Nonce::from_entropy_source(entropy); - let hmac = signer::hmac_for_held_htlc_available_context(nonce, expanded_key); let path_absolute_expiry = Duration::from_secs(inbound_payment::calculate_absolute_expiry( created_at.as_secs(), relative_expiry_secs, )); let context = MessageContext::AsyncPayments(AsyncPaymentsContext::InboundPayment { - nonce, - hmac, path_absolute_expiry, }); @@ -927,7 +894,6 @@ where R::Target: Router, { let entropy = &*entropy_source; - let expanded_key = &self.inbound_payment_key; let secp_ctx = &self.secp_ctx; let relative_expiry = DEFAULT_RELATIVE_EXPIRY.as_secs() as u32; @@ -989,13 +955,8 @@ where match response { Ok(invoice) => { - let nonce = Nonce::from_entropy_source(entropy); - let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key); - let context = MessageContext::Offers(OffersContext::InboundPayment { - payment_hash, - nonce, - hmac, - }); + let context = + MessageContext::Offers(OffersContext::InboundPayment { payment_hash }); (OffersMessage::Invoice(invoice), Some(context)) }, @@ -1032,14 +993,7 @@ where &self, invoice_request: InvoiceRequest, payment_id: PaymentId, nonce: Nonce, peers: Vec, ) -> Result<(), Bolt12SemanticError> { - let expanded_key = &self.inbound_payment_key; - - let hmac = payment_id.hmac_for_offer_payment(nonce, expanded_key); - let context = MessageContext::Offers(OffersContext::OutboundPayment { - payment_id, - nonce, - hmac: Some(hmac), - }); + let context = MessageContext::Offers(OffersContext::OutboundPayment { payment_id, nonce }); let reply_paths = self .create_blinded_paths(peers, context) .map_err(|_| Bolt12SemanticError::MissingPaths)?; @@ -1080,23 +1034,12 @@ where /// to create blinded reply paths /// /// [`supports_onion_messages`]: crate::types::features::Features::supports_onion_messages - pub fn enqueue_invoice( - &self, entropy_source: ES, invoice: Bolt12Invoice, refund: &Refund, - peers: Vec, - ) -> Result<(), Bolt12SemanticError> - where - ES::Target: EntropySource, - { - let expanded_key = &self.inbound_payment_key; - let entropy = &*entropy_source; - + pub fn enqueue_invoice( + &self, invoice: Bolt12Invoice, refund: &Refund, peers: Vec, + ) -> Result<(), Bolt12SemanticError> { let payment_hash = invoice.payment_hash(); - let nonce = Nonce::from_entropy_source(entropy); - let hmac = payment_hash.hmac_for_offer_payment(nonce, expanded_key); - - let context = - MessageContext::Offers(OffersContext::InboundPayment { payment_hash, nonce, hmac }); + let context = MessageContext::Offers(OffersContext::InboundPayment { payment_hash }); let reply_paths = self .create_blinded_paths(peers, context) @@ -1157,23 +1100,11 @@ where /// [`ReleaseHeldHtlc`]: crate::onion_message::async_payments::ReleaseHeldHtlc /// [`supports_onion_messages`]: crate::types::features::Features::supports_onion_messages #[cfg(async_payments)] - pub fn enqueue_held_htlc_available( - &self, entropy_source: ES, invoice: &StaticInvoice, payment_id: PaymentId, - peers: Vec, - ) -> Result<(), Bolt12SemanticError> - where - ES::Target: EntropySource, - { - let expanded_key = &self.inbound_payment_key; - let entropy = &*entropy_source; - - let nonce = Nonce::from_entropy_source(entropy); - let hmac = payment_id.hmac_for_async_payment(nonce, expanded_key); - let context = MessageContext::AsyncPayments(AsyncPaymentsContext::OutboundPayment { - payment_id, - nonce, - hmac, - }); + pub fn enqueue_held_htlc_available( + &self, invoice: &StaticInvoice, payment_id: PaymentId, peers: Vec, + ) -> Result<(), Bolt12SemanticError> { + let context = + MessageContext::AsyncPayments(AsyncPaymentsContext::OutboundPayment { payment_id }); let reply_paths = self .create_blinded_paths(peers, context) diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index 329b90d2076..6c0fddba6d3 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -21,7 +21,6 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::{Hash, HashEngine}; use bitcoin::secp256k1::{self, Keypair, PublicKey, Secp256k1, SecretKey}; use core::fmt; -use types::payment::PaymentHash; use crate::prelude::*; @@ -38,23 +37,25 @@ const DERIVED_METADATA_AND_KEYS_HMAC_INPUT: &[u8; 16] = &[2; 16]; const WITHOUT_ENCRYPTED_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[3; 16]; const WITH_ENCRYPTED_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[4; 16]; -// HMAC input for a `PaymentId`. The HMAC is used in `OffersContext::OutboundPayment`. -const OFFER_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[5; 16]; -// HMAC input for a `PaymentId`. The HMAC is used in `AsyncPaymentsContext::OutboundPayment`. -#[cfg(async_payments)] -const ASYNC_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[6; 16]; - -// HMAC input for a `PaymentHash`. The HMAC is used in `OffersContext::InboundPayment`. -const PAYMENT_HASH_HMAC_INPUT: &[u8; 16] = &[7; 16]; +// NOTE: +// The following `HMAC_INPUT` constants were previously used for authenticating fields +// in `OffersContext` and `AsyncPaymentsContext`, but were removed in LDK v0.1.X with +// the introduction of `ReceiveAuthKey`-based authentication. +// Their corresponding values (`[5; 16]`, `[6; 16]`, `[7; 16]`, and `[9; 16]`) are now +// reserved and must not be reused to preserve backward compatibility. +// +// Reserved HMAC_INPUT values — do not reuse: +// +// const OFFER_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[5; 16]; +// #[cfg(async_payments)] +// const ASYNC_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[6; 16]; +// const PAYMENT_HASH_HMAC_INPUT: &[u8; 16] = &[7; 16]; +// #[cfg(async_payments)] +// const ASYNC_PAYMENTS_HELD_HTLC_HMAC_INPUT: &[u8; 16] = &[9; 16]; // HMAC input for `ReceiveTlvs`. The HMAC is used in `blinded_path::payment::PaymentContext`. const PAYMENT_TLVS_HMAC_INPUT: &[u8; 16] = &[8; 16]; -// HMAC input used in `AsyncPaymentsContext::InboundPayment` to authenticate inbound -// held_htlc_available onion messages. -#[cfg(async_payments)] -const ASYNC_PAYMENTS_HELD_HTLC_HMAC_INPUT: &[u8; 16] = &[9; 16]; - /// Message metadata which possibly is derived from [`MetadataMaterial`] such that it can be /// verified. #[derive(Clone)] @@ -453,76 +454,6 @@ fn hmac_for_message<'a>( Ok(hmac) } -pub(crate) fn hmac_for_offer_payment_id( - payment_id: PaymentId, nonce: Nonce, expanded_key: &ExpandedKey, -) -> Hmac { - hmac_for_payment_id(payment_id, nonce, OFFER_PAYMENT_ID_HMAC_INPUT, expanded_key) -} - -pub(crate) fn verify_offer_payment_id( - payment_id: PaymentId, hmac: Hmac, nonce: Nonce, expanded_key: &ExpandedKey, -) -> Result<(), ()> { - if hmac_for_offer_payment_id(payment_id, nonce, expanded_key) == hmac { - Ok(()) - } else { - Err(()) - } -} - -pub(crate) fn hmac_for_payment_hash( - payment_hash: PaymentHash, nonce: Nonce, expanded_key: &ExpandedKey, -) -> Hmac { - const IV_BYTES: &[u8; IV_LEN] = b"LDK Payment Hash"; - let mut hmac = expanded_key.hmac_for_offer(); - hmac.input(IV_BYTES); - hmac.input(&nonce.0); - hmac.input(PAYMENT_HASH_HMAC_INPUT); - hmac.input(&payment_hash.0); - - Hmac::from_engine(hmac) -} - -pub(crate) fn verify_payment_hash( - payment_hash: PaymentHash, hmac: Hmac, nonce: Nonce, expanded_key: &ExpandedKey, -) -> Result<(), ()> { - if hmac_for_payment_hash(payment_hash, nonce, expanded_key) == hmac { - Ok(()) - } else { - Err(()) - } -} - -#[cfg(async_payments)] -pub(crate) fn hmac_for_async_payment_id( - payment_id: PaymentId, nonce: Nonce, expanded_key: &ExpandedKey, -) -> Hmac { - hmac_for_payment_id(payment_id, nonce, ASYNC_PAYMENT_ID_HMAC_INPUT, expanded_key) -} - -#[cfg(async_payments)] -pub(crate) fn verify_async_payment_id( - payment_id: PaymentId, hmac: Hmac, nonce: Nonce, expanded_key: &ExpandedKey, -) -> Result<(), ()> { - if hmac_for_async_payment_id(payment_id, nonce, expanded_key) == hmac { - Ok(()) - } else { - Err(()) - } -} - -fn hmac_for_payment_id( - payment_id: PaymentId, nonce: Nonce, hmac_input: &[u8; 16], expanded_key: &ExpandedKey, -) -> Hmac { - const IV_BYTES: &[u8; IV_LEN] = b"LDK Payment ID ~"; - let mut hmac = expanded_key.hmac_for_offer(); - hmac.input(IV_BYTES); - hmac.input(&nonce.0); - hmac.input(hmac_input); - hmac.input(&payment_id.0); - - Hmac::from_engine(hmac) -} - pub(crate) fn hmac_for_payment_tlvs( receive_tlvs: &UnauthenticatedReceiveTlvs, nonce: Nonce, expanded_key: &ExpandedKey, ) -> Hmac { @@ -546,27 +477,3 @@ pub(crate) fn verify_payment_tlvs( Err(()) } } - -#[cfg(async_payments)] -pub(crate) fn hmac_for_held_htlc_available_context( - nonce: Nonce, expanded_key: &ExpandedKey, -) -> Hmac { - const IV_BYTES: &[u8; IV_LEN] = b"LDK Held HTLC OM"; - let mut hmac = expanded_key.hmac_for_offer(); - hmac.input(IV_BYTES); - hmac.input(&nonce.0); - hmac.input(ASYNC_PAYMENTS_HELD_HTLC_HMAC_INPUT); - - Hmac::from_engine(hmac) -} - -#[cfg(async_payments)] -pub(crate) fn verify_held_htlc_available_context( - nonce: Nonce, hmac: Hmac, expanded_key: &ExpandedKey, -) -> Result<(), ()> { - if hmac_for_held_htlc_available_context(nonce, expanded_key) == hmac { - Ok(()) - } else { - Err(()) - } -}