-
Notifications
You must be signed in to change notification settings - Fork 47
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
base: master
Are you sure you want to change the base?
Changes from all commits
2e66fcf
1c3c43c
6ba8e68
fe5bed9
404eab9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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`] | ||
|
||
The initiator: | ||
* MUST set `handshake_count` to a number greater than its trusted peer set count, using round | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The message sender needs the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, maybe it wasn't clear, the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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`] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 :) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> |
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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.