Skip to content

Introduce ReceiveAuthKey #3917

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jul 18, 2025
Merged

Introduce ReceiveAuthKey #3917

merged 9 commits into from
Jul 18, 2025

Conversation

shaavan
Copy link
Member

@shaavan shaavan commented Jul 8, 2025

Builds on #3845

This PR builds on the work in #3845 by introducing ReceiveAuthKey, a dedicated struct that replaces the previously hardcoded [u8; 32] used for authenticating MessageContexts in incoming BlindedMessagePaths.

It:

  • Encapsulates the authentication key in a type-safe ReceiveAuthKey struct
  • Adds this key to the NodeSigner interface
  • Updates the BlindedMessagePath constructor to accept it as a parameter

This completes the original intent of #3845 — making the authentication mechanism explicit and less byte-heavy, while preserving the same security properties.

Next steps:

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Jul 8, 2025

👋 Thanks for assigning @TheBlueMatt as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@shaavan
Copy link
Member Author

shaavan commented Jul 8, 2025

cc @TheBlueMatt

@shaavan
Copy link
Member Author

shaavan commented Jul 9, 2025

Updated from pr3917.01 to pr3917.02 (diff):

Changes:

  1. Removed now redundant (hmac, nonce), from MessageContext

@shaavan
Copy link
Member Author

shaavan commented Jul 9, 2025

Updated from pr3917.02 to pr3917.03 (diff):

Changes:

  1. Rebase on main to resolve merge conflicts.

Copy link

codecov bot commented Jul 9, 2025

Codecov Report

Attention: Patch coverage is 96.69749% with 25 lines in your changes missing coverage. Please review.

Project coverage is 88.85%. Comparing base (42085b9) to head (9ce5b19).

Files with missing lines Patch % Lines
lightning-dns-resolver/src/lib.rs 44.44% 10 Missing ⚠️
lightning/src/crypto/streams.rs 90.32% 2 Missing and 4 partials ⚠️
lightning/src/onion_message/messenger.rs 92.59% 4 Missing ⚠️
lightning/src/ln/blinded_payment_tests.rs 88.88% 2 Missing ⚠️
lightning/src/ln/channelmanager.rs 75.00% 1 Missing and 1 partial ⚠️
lightning/src/crypto/poly1305.rs 99.66% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3917      +/-   ##
==========================================
+ Coverage   88.84%   88.85%   +0.01%     
==========================================
  Files         166      166              
  Lines      119487   119651     +164     
  Branches   119487   119651     +164     
==========================================
+ Hits       106155   106321     +166     
+ Misses      11008    11003       -5     
- Partials     2324     2327       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically LGTM, a few minor nits.

Also, the last commit is beautiful:
4 files changed, 34 insertions(+), 331 deletions(-)

@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @valentinewallace! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@TheBlueMatt
Copy link
Collaborator

Oops, needs rebase.

@shaavan
Copy link
Member Author

shaavan commented Jul 10, 2025

Updated from pr3917.03 to pr3917.04 (diff):
Addressed @TheBlueMatt comments

Changes:

  1. Code, and commit history clean-ups
  2. Introduce cleaner documentation for the removed fields and constant, for better future reference.

@shaavan
Copy link
Member Author

shaavan commented Jul 10, 2025

Also, the last commit is beautiful:
4 files changed, 34 insertions(+), 331 deletions(-)

Haha, just a direct consequence of standing on the shoulders of giants. Thanks so much for the amazing ReceiveAuthKey-based authentication system! ✨

@shaavan
Copy link
Member Author

shaavan commented Jul 10, 2025

Updated from pr3917.04 to pr3917.05 (diff):

Changes:

  1. Rebase on main to resolve merge conflicts.

Copy link
Contributor

@valentinewallace valentinewallace left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finished a pass, looks solid! Still need to review the fuzzing code moves in detail. Will take another pass also looking closely at the dual read adapter again next week!

Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this needs rebase now, sorry about that. Once you do that and address #3917 (comment) I think this should be about ready to go.

Comment on lines +44 to +47
#[cfg(not(fuzzing))]
let mut mac = Poly1305::new(&mac_key[..32]);
#[cfg(fuzzing)]
let mut mac = Poly1305::new(&key);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this a bit confusing. Seems we can avoid it by updating the full_stack fuzzer tests to use zeroed-out MACs. This fixes the test_gossip_exchange_breakage test, similar changes would be needed in test_no_existing_test_breakage.

diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs
index 1d16b5c4a..db9d05f68 100644
--- a/fuzz/src/full_stack.rs
+++ b/fuzz/src/full_stack.rs
@@ -1579,41 +1579,40 @@ fn gossip_exchange_seed() -> Vec<u8> {
 	// inbound read from peer id 0 of len 50
 	ext_from_hex("030032", &mut test);
 	// noise act two (0||pubkey||mac)
-	ext_from_hex("00 030000000000000000000000000000000000000000000000000000000000000002 03000000000000000000000000000000", &mut test);
+	ext_from_hex("00 030000000000000000000000000000000000000000000000000000000000000002 00000000000000000000000000000000", &mut test);
 
 	// inbound read from peer id 0 of len 18
 	ext_from_hex("030012", &mut test);
 	// message header indicating message length 16
-	ext_from_hex("0010 03000000000000000000000000000000", &mut test);
+	ext_from_hex("0010 00000000000000000000000000000000", &mut test);
 	// inbound read from peer id 0 of len 32
 	ext_from_hex("030020", &mut test);
 	// init message (type 16) with static_remotekey required, no channel_type/anchors/taproot, and other bits optional and mac
-	ext_from_hex("0010 00021aaa 0008aaa20aaa2a0a9aaa 03000000000000000000000000000000", &mut test);
+	ext_from_hex("0010 00021aaa 0008aaa20aaa2a0a9aaa 00000000000000000000000000000000", &mut test);
 
 	// new inbound connection with id 1
 	ext_from_hex("01", &mut test);
 	// inbound read from peer id 1 of len 50
 	ext_from_hex("030132", &mut test);
 	// inbound noise act 1
-	ext_from_hex("0003000000000000000000000000000000000000000000000000000000000000000703000000000000000000000000000000", &mut test);
-	// inbound read from peer id 1 of len 66
+	ext_from_hex("0003000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000", &mut test); // inbound read from peer id 1 of len 66
 	ext_from_hex("030142", &mut test);
 	// inbound noise act 3
-	ext_from_hex("000302000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000003000000000000000000000000000000", &mut test);
+	ext_from_hex("000302000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", &mut test);
 
 	// inbound read from peer id 1 of len 18
 	ext_from_hex("030112", &mut test);
 	// message header indicating message length 16
-	ext_from_hex("0010 01000000000000000000000000000000", &mut test);
+	ext_from_hex("0010 00000000000000000000000000000000", &mut test);
 	// inbound read from peer id 1 of len 32
 	ext_from_hex("030120", &mut test);
 	// init message (type 16) with static_remotekey required, no channel_type/anchors/taproot, and other bits optional and mac
-	ext_from_hex("0010 00021aaa 0008aaa20aaa2a0a9aaa 01000000000000000000000000000000", &mut test);
+	ext_from_hex("0010 00021aaa 0008aaa20aaa2a0a9aaa 00000000000000000000000000000000", &mut test);
 
 	// inbound read from peer id 0 of len 18
 	ext_from_hex("030012", &mut test);
 	// message header indicating message length 432
-	ext_from_hex("01b0 03000000000000000000000000000000", &mut test);
+	ext_from_hex("01b0 00000000000000000000000000000000", &mut test);
 	// inbound read from peer id 0 of len 255
 	ext_from_hex("0300ff", &mut test);
 	// First part of channel_announcement (type 256)
@@ -1621,25 +1620,25 @@ fn gossip_exchange_seed() -> Vec<u8> {
 	// inbound read from peer id 0 of len 193
 	ext_from_hex("0300c1", &mut test);
 	// Last part of channel_announcement and mac
-	ext_from_hex("020202 00006fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000000000000000002a030303030303030303030303030303030303030303030303030303030303030303020202020202020202020202020202020202020202020202020202020202020202030303030303030303030303030303030303030303030303030303030303030303020202020202020202020202020202020202020202020202020202020202020202 03000000000000000000000000000000", &mut test);
+	ext_from_hex("020202 00006fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000000000000000002a030303030303030303030303030303030303030303030303030303030303030303020202020202020202020202020202020202020202020202020202020202020202030303030303030303030303030303030303030303030303030303030303030303020202020202020202020202020202020202020202020202020202020202020202 00000000000000000000000000000000", &mut test);
 
 	// inbound read from peer id 0 of len 18
 	ext_from_hex("030012", &mut test);
 	// message header indicating message length 138
-	ext_from_hex("008a 03000000000000000000000000000000", &mut test);
+	ext_from_hex("008a 00000000000000000000000000000000", &mut test);
 	// inbound read from peer id 0 of len 154
 	ext_from_hex("03009a", &mut test);
 	// channel_update (type 258) and mac
-	ext_from_hex("0102 00000000000000000000000000000000000000000000000000000000000000a60303030303030303030303030303030303030303030303030303030303030303 6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000 000000000000002a0000002c01000028000000000000000000000000000000000000000005f5e100 03000000000000000000000000000000", &mut test);
+	ext_from_hex("0102 00000000000000000000000000000000000000000000000000000000000000a60303030303030303030303030303030303030303030303030303030303030303 6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000 000000000000002a0000002c01000028000000000000000000000000000000000000000005f5e100 00000000000000000000000000000000", &mut test);
 
 	// inbound read from peer id 0 of len 18
 	ext_from_hex("030012", &mut test);
 	// message header indicating message length 142
-	ext_from_hex("008e 03000000000000000000000000000000", &mut test);
+	ext_from_hex("008e 00000000000000000000000000000000", &mut test);
 	// inbound read from peer id 0 of len 158
 	ext_from_hex("03009e", &mut test);
 	// node_announcement (type 257) and mac
-	ext_from_hex("0101 00000000000000000000000000000000000000000000000000000000000000280303030303030303030303030303030303030303030303030303030303030303 00000000002b03030303030303030303030303030303030303030303030303030303030303030300000000000000000000000000000000000000000000000000000000000000000000000000 03000000000000000000000000000000", &mut test);
+	ext_from_hex("0101 00000000000000000000000000000000000000000000000000000000000000280303030303030303030303030303030303030303030303030303030303030303 00000000002b03030303030303030303030303030303030303030303030303030303030303030300000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000", &mut test);
 
 	test
 }
diff --git a/lightning/src/crypto/chacha20poly1305rfc.rs b/lightning/src/crypto/chacha20poly1305rfc.rs
index 839fad9ce..f96869051 100644
--- a/lightning/src/crypto/chacha20poly1305rfc.rs
+++ b/lightning/src/crypto/chacha20poly1305rfc.rs
@@ -41,10 +41,10 @@ impl ChaCha20Poly1305RFC {
 		let zero_key = [0u8; 64];
 		cipher.process(&zero_key, &mut mac_key);
 
-		#[cfg(not(fuzzing))]
+		// #[cfg(not(fuzzing))]
 		let mut mac = Poly1305::new(&mac_key[..32]);
-		#[cfg(fuzzing)]
-		let mut mac = Poly1305::new(&key);
+		// #[cfg(fuzzing)]
+		// let mut mac = Poly1305::new(&key);
 		mac.input(aad);
 		ChaCha20Poly1305RFC::pad_mac_16(&mut mac, aad.len());
 

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can also do this in follow-up. Not a huge deal but would prefer to either do this or at least have a doc comment on this fuzzing-gated code.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessarily changing the full_stack_target kinda sucks cause it breaks all the existing corpus files that exist. I'd kinda rather not :/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I guess that also already happens whenever a new feerate call is added? The diff above is a more significant change to the hardcoded strings though. Adding a comment seems fine too.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, it does happen somewhat frequently, though this breaks all the messages too, so was kinda trying to avoid it. I don't think its critical, but I just try to avoid where possible.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TheBlueMatt,

I was looking through this part of code and came across something that felt related to this, so I just wanted to bring it to attention.

I noticed that the ChaCha20Poly1305RFC constructor accepts either a 16 or 32-byte key:

assert!(key.len() == 16 || key.len() == 32);

But in the fuzzing implementation of Poly1305, the constructor always asserts a 32-byte key:

impl Poly1305 {
    pub fn new(key: &[u8]) -> Poly1305 {
        assert_eq!(key.len(), 32);
        ...
    }
}

I'm not sure why the fuzz code is set up this way. Would love to understand the reasoning behind it.
Thanks a lot! ✨

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT we always pass in a 32-byte key, so not sure if it's necessary to allow 16-byte keys. It's a fuzzing only potential issue so don't think it's a huge concern; in non-fuzzing builds, it looks like the code works when a 16-byte key is used. I'm guessing we support 16-byte keys in ChaCha20Poly1305RFC because that complies with the RFC.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it! Since it’s just a fuzzing-only quirk, we can probably address it in a follow-up if it ever turns into an issue.
Thanks so much for the clarification! ✨

Copy link
Contributor

@valentinewallace valentinewallace left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM after outstanding feedback. Let me know if you don't have time for this @shaavan, I'm going to be OOO next week so would like to get this in

`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.
@shaavan
Copy link
Member Author

shaavan commented Jul 17, 2025

Updated from pr3917.05 to pr3917.06 (diff):

Changes:

  1. Rebase on main to resolve merge conflicts

@shaavan
Copy link
Member Author

shaavan commented Jul 17, 2025

Updated from pr3917.06 to pr3917.07 (diff):
Addressed @TheBlueMatt, @valentinewallace comments

Changes:

  1. Various documentation and code clean-ups.
  2. Introduce usage of control_tlvs_authenticated in code.

TheBlueMatt and others added 3 commits July 17, 2025 21:59
`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.
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.
@shaavan
Copy link
Member Author

shaavan commented Jul 17, 2025

Updated from pr3917.07 to pr3917.08 (diff):
Addressed @valentinewallace comments

Changes:

  1. Renamed mod poly1305 to fix linting ci
  2. Updated peel_onion_message logic.

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()?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops this is wrong, this bypasses the chacha_stream which means the extra data won't be included in the MAC but should be. This is an existing bug, however, in ChaChaPolyReadAdapter, so needs fixing in both.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we just error if any bytes remain, since it means the length we were given for the data was wrong?

Comment on lines 1095 to 1140
// Note: We introduced the `control_tlvs_authenticated` check in LDK v0.2
// to simplify and standardize onion message authentication.
// To continue supporting offers/refunds created before v0.2, we allow
// unauthenticated control TLVs for these messages, as they were verified
// using the legacy method.
Ok(PeeledOnion::Offers(msg, Some(ctx), reply_path))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want to push the logic down into OnionMessenger, rather we should expose the authenticated bit to the message handler which can decide whether we wanted an authenticated BP or not there.

@valentinewallace
Copy link
Contributor

valentinewallace commented Jul 17, 2025

Pushed some docs updates and support for legacy refunds. Diff

Edit: pushed one more change adding a newline to a doc to cause a paragraph break

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]`.

Co-authored-by: Shashwat Vangani <[email protected]>
@valentinewallace
Copy link
Contributor

Removed legacy refund support and some references to async payments cfg-gated code. Diff

TheBlueMatt
TheBlueMatt previously approved these changes Jul 18, 2025
Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. A few things I'll follow up on, but this LGTM. It also had three people looking at it/working on it, so just gonna go ahead and land it.

@@ -3761,7 +3707,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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rustfmt fails here.

Copy link
Contributor

@valentinewallace valentinewallace Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird, this method is #[rustfmt::skip]'d. Fixed.

diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index 8331ed280..b873644ac 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -16431,7 +16431,8 @@ where
                        our_network_pubkey,
                        highest_seen_timestamp,
                        expanded_inbound_key,
-                       args.node_signer.get_receive_auth_key(), secp_ctx.clone(),
+                       args.node_signer.get_receive_auth_key(),
+                       secp_ctx.clone(),
                        args.message_router,
                )
                .with_async_payments_offers_cache(async_receive_offer_cache);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upstream it was removed, I believe, so it will fail once this lands :)

shaavan and others added 3 commits July 17, 2025 22:06
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.
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.
In the previous commit, we stopped authenticating incoming onion messages via
an explicit nonce and hmac encoded in the message context, and entirely
switched to authenticating via the new NodeSigner::get_receive_auth_key more or
less included as AAD in the ChaCha20Poly1305 de/encoding for the messages. As
such, the message handler docs need updating to describe the new authentication
scheme.
@TheBlueMatt TheBlueMatt merged commit d82af8c into lightningdevkit:main Jul 18, 2025
27 of 28 checks passed
@github-project-automation github-project-automation bot moved this from Goal: Merge to Done in Weekly Goals Jul 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
weekly goal Someone wants to land this this week
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

4 participants