Skip to content
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

Add blip-0031, a protocol for mutual message exchange #31

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions blip-0031.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
```
bLIP: 31
Title: Mutual Message Exchange
Status: Active
Author: Matt Corallo <[email protected]>
Created: 2024-01-14
License: CC0
```

## Abstract

This bLIP defines a simple protocol by which a paying party can include an encrypted message for a
payment recipient iff both parties have indicated they wish to communicate with each other.

## Copyright

This bLIP is licensed under the CC0 license.

## Specification

This protocol is defined in terms of two parties: (a) the initiator, and (b) the message-sender. In
the lightning context, the message-sender is the payment sender, and the initiator is the payment
recipient.

Each party is configured with a private key (`k`) and a list of acceptable public keys they wish to
exchange messages with (`P`).

Each party stores the set `s` of the result of `H(ECDH_SALT || H(ECDH(k, P_i)))` for each `P_i` in `P`.

The initiator picks a random 32-byte nonce `initiator_n`. It then generates the init bytes, which are:
* [`u16`:`handshake_count`]
* [`48*handshake_count*byte`:`per_peer_handshake`]
* [`u16`:`repeated_data_len`]
* [`repeated_data_len*byte`:`repeated_data`]
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you have an example on what state you'd store here? Does this generally need to be encrypted/authenticated by the initiator to themselves (even though at the bLIP level we don't want to constrain higher-level protocols)?

Copy link
Contributor Author

@TheBlueMatt TheBlueMatt Jan 15, 2024

Choose a reason for hiding this comment

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

In the LDK implementation we store (encrypted+authenticated) a random nonce from which we derive other per-message secrets (notably the initiator_n). This makes the sending side stateless, which is nice. Indeed we don't want to constraint the protocol itself to that.


The initiator:
* MUST set `handshake_count` to a number greater than its trusted peer set count, using round
Copy link

Choose a reason for hiding this comment

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

Can u please explain how setting round value here help with not revealing the total count? or is it "random"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A "round value" specifically doesn't help, but what we want is if one node trusts 20 peers and another 19, both should send the same number of slots so that it looks the same.

values to avoid revealing how many trusted peers it has configured.
* MUST fill in each `per_peer_handshake` with either:
* pseudorandom data for any excess unused trusted peer entries,
* `AEAD(initiator_n, s_i, PROTO_SALT, PROTO_AAD)` for each `s_i` in the initiator's `s` set.
* MAY include `repeated_data` which will be received with the message to avoid storing state.
* MUST set `repeated_data_len` to the length of `repeated_data`, if any.

The message-sender:
* MUST ignore any bytes which are included after the end of `repeated_data`.
* SHOULD walk each `per_peer_handshake` and check whether it is decryptable using each `s_i` it
Copy link
Contributor

Choose a reason for hiding this comment

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

The message sender needs the initiator_n for that, right? How is it transmitted?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, maybe it wasn't clear, the per_peer_handshake is an encrypted copy of the initiator_n, using the ECDH as a key.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated the "what is AEAD" bit in the spec.

has stored.
* If a `per_peer_handshake` is decryptable, should send a response including the message.

The message sender similarly picks a random 32-byte nonce `messenger_n` and responds with:
* [`u16`:`handshake_index`]
* [`48*byte`:`encrypted_nonce`]
* [`u16`:`repeated_data_len`]
* [`repeated_data_len*byte`:`repeated_data`]
* [`u16`:`message_length`]
Copy link

Choose a reason for hiding this comment

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

May I ask why in the protocol level we save the length of a variable as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It allows us to append arbitrary stuff at the end of the message later. Means the reader knows where to stop reading and where the next field starts.

* [`message_length*byte`:`encrypted_message`]

The message-sender:
* MUST set `handshake_index` to the index of the `per_peer_handshake` they were able to decrypt
within the init bytes.
* MUST set `encrypted_nonce` to
`AEAD(messenger_n, H(s_i || initiator_n), PROTO_SALT ^ MASK_A, PROTO_AAD)`
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need MASK_A and MASK_B?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We dont, there's a handful of things here I don't think we need, but I prefer to have rather than try to concretely prove we dont need :)

Copy link
Contributor

Choose a reason for hiding this comment

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

But can you share the reasoning behind why you added this in the first place? I'm curious as I don't obviously see it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, just on principle I like the idea of every line of code which instantiates a given cipher to have a different salt. We're not reusing keys so its not actually required, but just on principle the point of a salt is to pass "what I'm doing with the output" in to the cipher, and that should be on a per-line-of-code basis.

* MUST include the same `repeated_data_len` and `repeated_data` as was sent by the initiator.
* MUST set `message_length` to the length of the message they wish to send, *including* the
16-byte MAC tag. If they only wish to communicate a message separately, `message_length` MUST be
set to 0.
* MAY calculate two message encryption keys by reading 64 bytes from
`ChaCha20(initiator_n ^ messenger^n, KEY_STRETCH_SALT)`. The first 32 bytes are `inline_msg_key`,
the next 32 bytes are `oob_msg_key`.
* If `message_length` is not zero, MUST set `encrypted_message` to
`AEAD(message, inline_msg_key, PROTO_SALT ^ MASK_B, PROTO_AAD)`
* MAY exchange further messages with the initiator, using the `oob_msg_key` to encrypt and
authenticate such messages.

The initiator (message recipient):
* If `message_length` is between 1 and 15 (inclusive), the handshake should be treated as having
failed.
* MUST ignore any bytes which are included after the end of `encrypted_message`.
* SHOULD decrypt the `encrypted_nonce` and use the contained `messenger_n` to decrypt the
`message`.
* MAY exchange further messages with the message-sender, using the `oob_msg_key` to encrypt and
authenticate such messages.

### Constants
* `H()` is SHA-256
* `ECDH()` is ECDH using secp256k1 public/secret keys.
* `AEAD(data, key, salt, aad)` is the ChaCha20Poly1305 RFC variant authenticated encryption
* `ECDH_SALT` is "Mutual Message Exchange ECDH Result"
* `KEY_STRETCH_SALT` is "INLINE KEY STRCH"
* `MASK_A` is all 0xff bytes
* `MASK_B` is all 0xf0 bytes
* `PROTO_SALT` is a salt picked to describe a specific protocol using this message exchange
* `PROTO_AAD` is excess data used to describe a specific instantiation of this protocol, for
example the specific TLV which is being used to communicate a particular type of message.

## Discussion

There are various times during which lightning senders may wish to include a message for the
recipient. In many cases, the sender wishes to include such a message no matter the recipient,
often accomplished by simply including that message as an additional TLV entry in the recipient's
onion. However, in some cases the sender wishes to only include their message if there is mutual
trust between the sender and the recipient.

Prior to BOLT12, this was still a rather simple problem - senders could validate the recipient
node_id against a list of parties they wished to message and then include their message in the
onion as they otherwise would. However, with BOLT12 node_ids are no longer immediately visible.
Worse, as the number of payment protocols continues to proliferate, senders need to have a list
of acceptable messaging peers in multiple formats, maintained separately.

This bLIP defines a simple protocol by which a paying party can include an encrypted message for a
payment recipient iff both parties have indicated they wish to communicate with each other. It only
requires the ability for the payment recipient to include additional bytes (~48 bytes per peer they
are willing to exchange messages with). From there, the sender only has to include an extra 100-200
bytes in addition to the message itself in the onion they ultimately use to send the payment.

If the sender is not in the recipient's trusted peers list, they will not learn anything about who
the recipient is. However if the recipient does trust the sender, the sender may be able to learn
who the recipient is even if the sender does not trust the recipient (and thus no message will be
exchanged).

This protocol is envisioned for use in BOLT12 payment exchanges, however it can be used in any
payment negotiation protocol, or even in non-payment contexts.

## Rationale

There are several alternative protocols which could be considered.

First, the protocol could be split into one additional message - instead of the initiator being the
payment recipient, the payment sender could initiate. This would result in less data being included
in the final HTLC, however it would result in most of the CPU cost being borne by the (potential)
payment recipient, rather than the payment sender. This could allow for nodes to initiate the
protocol repeatedly with a victim, DoSing them without ever sending a payment.

Second, the protocol could be made further private by excluding the MACs in the initiator's bytes.
This would result in the responder being forced to include `N*M` `encrypted_nonce`s in their
response (implying the first change above as it would now be too large to include in an onion). This
would result in the responder not learning whether they were in the initiator's trusted set unless
they also trusted the initiator, however at the cost of substantial overhead. Because the
anticipated use cases of this protocol imply mutual trust, this is not considered worth the
trade-off. Further, this would also present challenges in situations where we only have a full RTT
available, namely in the BOLT12 `refund` case.

## Reference Implementation
* LDK: <https://github.com/lightningdevkit/rust-lightning/pull/2829>