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

blip-0042: Bolt 12 Contacts #42

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

t-bast
Copy link
Contributor

@t-bast t-bast commented Jul 19, 2024

Bolt 12 introduces offers, which are static lightning "addresses". An offer can be stored and reused to pay the same node many times. It then becomes natural to associate Bolt 12 offers to your friends and contacts.

When sending payments to contacts, you may want them to know that the payment came from you. We propose a scheme to optionally include contact information in outgoing payments to allow the recipient to:

  • detect that the payment is coming from one of their known contacts
  • otherwise, be able to add the payer to their contacts list
  • send funds back to the payer without additional interaction

This feature provides a better UX for lightning wallets, by making payments between contacts look very similar to fiat payment
applications.

@t-bast t-bast changed the title blip-0042: Bolt 12 contacts blip-0042: Bolt 12 Contacts Jul 19, 2024
@t-bast t-bast force-pushed the deterministic-contact-payer-key branch from 9925dac to a5199f7 Compare July 19, 2024 15:26
@t-bast t-bast marked this pull request as ready for review July 19, 2024 15:27
blip-0042.md Outdated Show resolved Hide resolved
blip-0042.md Outdated Show resolved Hide resolved
@TheBlueMatt
Copy link
Contributor

One downside here is it relies on the recipient using a static offer signing pubkey, which by default they almost certainly should not do. A radically different approach would be to use something like bLIP #31 to use separate key material from the offer id to do a mutual handshake without using static keys.

@t-bast
Copy link
Contributor Author

t-bast commented Jul 22, 2024

One downside here is it relies on the recipient using a static offer signing pubkey, which by default they almost certainly should not do. A radically different approach would be to use something like bLIP #31 to use separate key material from the offer id to do a mutual handshake without using static keys.

It is very important in my opinion that "adding a contact" is as simple as entering a BIP 353 address or scanning a QR code and clicking a "store contact" button. It is also very important to make it easy to add to any wallet to guarantee compatibility across the ecosystem.

The simplest way I can see to achieve that is to use key material from the Bolt 12 offer directly. This is why I chose to use a key that already needs to be in any valid offer, even though the drawback is that it's a bit of a layering violation (by deriving a key used for paying from a key used to get paid).

It seems to me that alternatives will:

  • be harder to deploy across the eco-system if we want the key material to be outside of the Bolt 12 offer
  • make Bolt 12 offers bigger (which we'd like to avoid) if we add separate key material inside the Bolt 12 offer

@TheBlueMatt
Copy link
Contributor

It is very important in my opinion that "adding a contact" is as simple as entering a BIP 353 address or scanning a QR code and clicking a "store contact" button. It is also very important to make it easy to add to any wallet to guarantee compatibility across the ecosystem.

The simplest way I can see to achieve that is to use key material from the Bolt 12 offer directly.

I don't think bLIP 32 breaks either of those?

This is why I chose to use a key that already needs to be in any valid offer, even though the drawback is that it's a bit of a layering violation (by deriving a key used for paying from a key used to get paid).

There's two issues here, not just the one -

(A) IMO the re-use of the offer signing pubkey as the key to use for this authentication is not just a layering violation but breaks potentially important use-cases (eg custodial services can't use this, doubly so if they have more than one node). But that's not to say you can't by default use the offer signing key as the authentication key...

For example, when you pay an offer, if the recipient is willing to be authenticated, the invoice can specify an alternative "authentication key"/BP which is used to identify this recipient. Then, if the server is willing to be authenticated they can send an onion message to the node and share their authentication key.

Because this authentication should only ever be used if both ends have the other one in their contact list, just using a commonly available key is not sufficient.

(B) but much more importantly, the use of the offer signing key as the authentication key completely breaks BOLT 12. You must not use the same offer signing key for all of your offers. A static public offer is one thing but for this to work when I scan an offer you also have to reuse the same signing key on every offer, allowing someone to match a fresh offer against my public BIP 353 one.

Further, I'm very afraid of this design because people are absolutely going to implement it in the naive way - always reuse the payer-key no matter who the recipient is - which is very very much not okay.

@t-bast
Copy link
Contributor Author

t-bast commented Jul 22, 2024

(B) but much more importantly, the use of the offer signing key as the authentication key completely breaks BOLT 12. You must not use the same offer signing key for all of your offers. A static public offer is one thing but for this to work when I scan an offer you also have to reuse the same signing key on every offer, allowing someone to match a fresh offer against my public BIP 353 one.

I don't understand, that's not at all what I'm specifying here? When you'd pay one of your trusted contact, you would use the offer signing key of the current offer you're using in your BIP 353 address (which you can rotate with a different signing key). But previous offers or offers you're using for other purposes have no reason to use the same key?

I don't think bLIP 32 breaks either of those?

You mean bLIP 31? It doesn't break those assumptions, but I don't see how you fetch/configure the "list of acceptable public keys they wish to exchange messages with" you're mentioning in there if those aren't included in Bolt 12 offers. Or do you mean that you'd like to use bLIP 31, but use the offer signing keys of your contacts as that "list of acceptable public keys" so that they are directly contained in the Bolt 12 offers or your contacts and in their BIP 353 address?

Because if the keys from that "list of acceptable public keys they wish to exchange messages with" does not come from Bolt 12 offers, then we need a new protocol to exchange those keys and somehow include them in BIP 353, which can quickly get messy and hard to get widespread compatibility...

So overall, I'm not at all against using bLIP 31 instead of what I'm proposing here, but I'd like to see a more detailed design that shows how to use it for contact authentication if you could write it down.

@TheBlueMatt
Copy link
Contributor

I don't understand, that's not at all what I'm specifying here? When you'd pay one of your trusted contact, you would use the offer signing key of the current offer you're using in your BIP 353 address (which you can rotate with a different signing key). But previous offers or offers you're using for other purposes have no reason to use the same key?

Sure they do? Having a "contact list" where I can pay someone in person and it doesn't show up as the person in my contacts list is weird? Or, inversely, having a "contact list" where I won't show up as the sender if you've paid you in person before but not via BIP 353 is weird?

More generally, I may occasionally change my BIP 353 for various reasons, and I don't want my entire contact feature to break just because I changed it? That seems like a weird user footgun.

You mean bLIP 31? It doesn't break those assumptions, but I don't see how you fetch/configure the "list of acceptable public keys they wish to exchange messages with" you're mentioning in there if those aren't included in Bolt 12 offers. Or do you mean that you'd like to use bLIP 31, but use the offer signing keys of your contacts as that "list of acceptable public keys" so that they are directly contained in the Bolt 12 offers or your contacts and in their BIP 353 address?

Sorry, yes, bLIP 31. Indeed, bLIP 31 doesn't solve the issue here, it just gives us a building block for doing this in a more mutual and private way. We still have to solve key distribution.

We could pick from a few different key distribution methods, depending on what properties we want, for example, if we assume that users know their own BIP 353 (or can figure it out in a reasonable way):

(a) when receiving an invoice_request with the BIP 353 destination fields filled in, a node derives a public key from the BIP 353 destination fields and some authentication secret key, including that public key in their invoice,
(b) when receiving an invoice for any payment destination which is in their contact list, a user stores the authentication public key,
(b) when paying, nodes use their known BIP 353 and their authentication secret to derive their authentication key and attempt to initiate a bLIP 31 session with all stored contact authentication public keys.

This allows payments to be identified even if they scan a QR code after storing a contact, but it does rely on using BIP 353 for the contact list (maybe that's the intended use anyway? Just means you can't store a contact from an offer directly). It also means recipients can't track senders unless they are mutually in each others' contacts, which I think is an important property. It also allows for multiple "identities" for one node, though the assumption that a node knows its BIP 353 may be strong.

@t-bast
Copy link
Contributor Author

t-bast commented Jul 23, 2024

More generally, I may occasionally change my BIP 353 for various reasons, and I don't want my entire contact feature to break just because I changed it? That seems like a weird user footgun.

That's a good point. I'm biased because in Phoenix we deterministically derive a "default" offer from the user's seed (which allows seamlessly restoring it to a different device), but I agree that this is only one out of many possible strategies for mobile wallets, and being able to rotate your default offer without breaking your contact lists is important. It could also make sense to give different offers to different people to more easily account for payments from different use-cases.

As you note, this does force us to decouple the "contact key" from offers themselves and thus may require re-thinking the UX for adding contacts a bit. Would it make sense to simply include a new "lnck" (LN contact key) field in BIP 21 URIs that contains our contact pubkey? This way we separate it from the offer itself, but our BIP 353 address can include it in the returned URI, and the QR code for receiving Bolt 12 payments shown by wallets could use this BIP 21 URI as well (instead encoding just the Bolt 12 offer).

@t-bast
Copy link
Contributor Author

t-bast commented Jul 30, 2024

Based on the discussions we had in this PR so far, I think it's worth discussing the various options before actually specifying a bLIP. I've started the discussion here, please take a look and tell me what you think: https://delvingbitcoin.org/t/bolt-12-trusted-contacts/1046

t-bast added a commit to ACINQ/lightning-kmp that referenced this pull request Oct 17, 2024
Add support for contacts as specified in bLIP 42. Contacts are mutually
authenticated using a 32-bytes random secret generated when first adding
a node to our contacts. When paying contacts, we include our own payment
information to allow them to pay us back and us to their contacts.

The benefit of this design is that offers stay private by default (they
don't include any contact information). It's only when we pay someone
we trust that we reveal contact information (which they are free to
ignore).

The drawback of this design is that if when both nodes independently add
each other to their contacts list, they generate a different contact
secret: users must manually associate incoming payments to an existing
contact to correctly identify incoming payments (by storing multiple
secrets for such contacts). This also happens when contacts use multiple
wallets, which will all use different contact secrets. I think this is
an acceptable trade-off to preserve privacy by default.

More details in the bLIP: lightning/blips#42
t-bast added a commit to ACINQ/lightning-kmp that referenced this pull request Oct 17, 2024
Add support for contacts as specified in bLIP 42. Contacts are mutually
authenticated using a 32-bytes random secret generated when first adding
a node to our contacts. When paying contacts, we include our own payment
information to allow them to pay us back and us to their contacts.

The benefit of this design is that offers stay private by default (they
don't include any contact information). It's only when we pay someone
we trust that we reveal contact information (which they are free to
ignore).

The drawback of this design is that if when both nodes independently add
each other to their contacts list, they generate a different contact
secret: users must manually associate incoming payments to an existing
contact to correctly identify incoming payments (by storing multiple
secrets for such contacts). This also happens when contacts use multiple
wallets, which will all use different contact secrets. I think this is
an acceptable trade-off to preserve privacy by default.

More details in the bLIP: lightning/blips#42
t-bast added a commit to ACINQ/lightning-kmp that referenced this pull request Oct 17, 2024
Add support for contacts as specified in bLIP 42. Contacts are mutually
authenticated using a 32-bytes random secret generated when first adding
a node to our contacts. When paying contacts, we include our own payment
information to allow them to pay us back and us to their contacts.

The benefit of this design is that offers stay private by default (they
don't include any contact information). It's only when we pay someone
we trust that we reveal contact information (which they are free to
ignore).

The drawback of this design is that if when both nodes independently add
each other to their contacts list, they generate a different contact
secret: users must manually associate incoming payments to an existing
contact to correctly identify incoming payments (by storing multiple
secrets for such contacts). This also happens when contacts use multiple
wallets, which will all use different contact secrets. I think this is
an acceptable trade-off to preserve privacy by default.

More details in the bLIP: lightning/blips#42
Bolt 12 introduces offers, which are static lightning "addresses".
An offer can be stored and reused to pay the same node many times.
It then becomes natural to associate Bolt 12 offers to your friends
and contacts.

When sending payments to contacts, you may want them to know that the
payment came from you. We propose a scheme to optionally include
contact information in outgoing payments to allow the recipient to:

- detect that the payment is coming from one of their known contacts
- otherwise, be able to add the payer to their contacts list
- send funds back to the payer without additional interaction

This feature provides a better UX for lightning wallets, by making
payments between contacts look very similar to fiat payment
applications.
@t-bast t-bast force-pushed the deterministic-contact-payer-key branch from a5199f7 to 5c0a128 Compare October 18, 2024 03:54
t-bast added a commit to ACINQ/lightning-kmp that referenced this pull request Oct 18, 2024
Add support for contacts as specified in bLIP 42. Contacts are mutually
authenticated using a 32-bytes random secret generated when first adding
a node to our contacts. When paying contacts, we include our own payment
information to allow them to pay us back and us to their contacts.

The benefit of this design is that offers stay private by default (they
don't include any contact information). It's only when we pay someone
we trust that we reveal contact information (which they are free to
ignore).

The drawback of this design is that if when both nodes independently add
each other to their contacts list, they generate a different contact
secret: users must manually associate incoming payments to an existing
contact to correctly identify incoming payments (by storing multiple
secrets for such contacts). This also happens when contacts use multiple
wallets, which will all use different contact secrets. I think this is
an acceptable trade-off to preserve privacy by default.

More details in the bLIP: lightning/blips#42
@t-bast
Copy link
Contributor Author

t-bast commented Oct 18, 2024

@jklein24 @TheBlueMatt I have completely re-written this bLIP based on the whiteboard session we had during the lightning summit with Rusty, this is now ready for review as I'm chasing concept ACK before adding this to Phoenix!

We don't use payer_ids anymore, don't modify offers, but rather include a new contact_secret field in our invoice_requests to contacts and some payment information to allow them to pay us back.

Comment on lines +76 to +77
- SHOULD NOT include `invreq_payer_offer`.
- SHOULD include `invreq_payer_bip_353_name` instead.
Copy link
Contributor

Choose a reason for hiding this comment

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

We should probably make this verifiable somehow, no? IMO it should not be the case that I can pretend to be someone else when paying. We could include a signature by the offer's signing key, I guess?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure that would fix anything: if I'm pretending to be someone else when paying you, the offer I would include in invreq_payer_offer would also be one of mine, so I'd be able to sign the invalid bip_353_name and you wouldn't be able to tell?

I think this scheme has to rely on information obtained outside of the protocol: whenever you add someone to your contacts list based on a payment you received from them, you must have verified somehow (e.g. in real-life or through some secure messaging) that this was indeed their payment, in which case you can trust that the invreq_payer_offer and inreq_payer_bip_353_name correctly belong to them?

Copy link
Contributor

@TheBlueMatt TheBlueMatt Oct 23, 2024

Choose a reason for hiding this comment

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

I'm not sure that would fix anything: if I'm pretending to be someone else when paying you, the offer I would include in invreq_payer_offer would also be one of mine, so I'd be able to sign the invalid bip_353_name and you wouldn't be able to tell?

No I was thinking you'd have to sign using the offer signing key from the BIP 353 entry. That'd be verifiable.

We don't need to make the offer verifiable, necessarily, because people don't identify themselves by the offer signing key, though it would be kinda nice imo if they were.

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 I was thinking you'd have to sign using the offer signing key from the BIP 353 entry. That'd be verifiable.

Got it, that would make sense, as it would allow reconciling payments based on BIP 353 HRNs.

What do you think of the following proposal: if the sender of invoice_request includes invreq_bip_353_name, it MUST set invreq_payer_id to the signing key of the BIP 353 offer. This way the invoice_request is signed by the BIP 353 offer signing key. Since the payer wants to reveal their identity, I don't see any drawback in using the payer_id for this?

The reason I'd like to avoid adding an additional signature field is because:

  • we don't want to resolve the BIP 353 DNS record when receiving invoice_request: we haven't been paid yet, so it would be a DoS vector (anyone could flood us with invoice_request and ignore the invoice), we only want to resolve it after receiving the payment
  • if we added a new signature field, we would then need to include it in the path_id in the invoice returned, to be able to verify it when receiving the payment, which uses an additional 64 bytes in a potentially already large path_id (constrained by the 1300 bytes onion size)
  • whereas we already need to include the invreq_payer_id in that path_id (to allow the payer to later provide a PoP), so we only need to check that it matches the BIP 353 offer signing key after receiving the payment

If you think we should still create a new field for that signature, then we may need to require BIP 353 names to be short, probably not more than 200 characters: in practice it should already be the case, but in theory it can be 2*255 characters long...am I over-thinking this when considering that we need to handle "large" BIP 353 names?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@TheBlueMatt could you take a look at my last comments? I'd like to finalize a prototype version of this, and this seems to be the main blocking point that would create backwards-compat issues!

blip-0042.md Outdated Show resolved Hide resolved
blip-0042.md Outdated
create a new contact.
- If it creates a new contact based on this received payment, the
received `contact_secret` MUST be used when paying that contact.
- MAY associate the received `contact_secret` with an existing contact.
Copy link
Contributor

Choose a reason for hiding this comment

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

How? Might want to specify "via BIP 353" because you generally can't based on the payer_offer unless it happens to exactly match one you already paid.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That was unclear indeed: I think we shouldn't automatically reconcile, that won't be helpful in practice and opens the door for exploitation if contact secrets leak. I updated this in e24d241 to mention that this is a manual process when the user has out-of-protocol knowledge that a payment came from a specific contact.

Copy link
Contributor

@TheBlueMatt TheBlueMatt Oct 23, 2024

Choose a reason for hiding this comment

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

I think we shouldn't automatically reconcile, that won't be helpful in practice and opens the door for exploitation if contact secrets leak.

I disagree. If we can verify a BIP 353 match (using the signing key in the offer in the DNS) then auto-reconciling would be a really nice UX compared to forcing users into a manual flow. It'll be really confusing UX when a user has a payment from a HRN and sees a different contact for sent payments to the same HRN.

I'm not all that worried about contact secret leaks, fwiw. Yes, we should make it so that you can't trivially steal money if they do, but at least on the BIP 353 end if you can edit someone's BIP 353 you can presumably steal some of their money anyway, soo....

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I disagree. If we can verify a BIP 353 match (using the signing key in the offer in the DNS)

I agree, as discussed in #42 (comment), if the BIP 353 name is signed, then we can reconcile using it.

blip-0042.md Show resolved Hide resolved
blip-0042.md Show resolved Hide resolved
blip-0042.md Outdated
- If it creates a new contact based on this received payment, the
received `contact_secret` MUST be used when paying that contact.
- MAY associate the received `contact_secret` with an existing contact.
- MUST ignore `invreq_payer_offer` and `invreq_bip_353_name` if it already
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we allow the offer to be updated if its using the same signing key? That way the blinded paths can be updated.

Copy link
Contributor Author

@t-bast t-bast Oct 21, 2024

Choose a reason for hiding this comment

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

I think this is dangerous: if the contact_secret was leaked, Mallory can create an offer that uses blinded paths to herself with the offer_issuer_id of Bob and include that in a payment to Alice. Then Alice would try to pay Mallory instead of Bob. Mallory wouldn't be able to receive the payments because she cannot sign invoices with Bob's offer_issuer_id, but she would see that Alice is trying to pay Bob, and she would prevent Alice from paying Bob.

I think the only way to refresh offers should be:

  • manually by the user (when they know that the offer they're adding is really from their contact)
  • through BIP 353 by resolving the DNS record

What do you think?

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't say I'm that worried about it? Lightning wallets have to store secret(s) that, if leaked, can lead to loss of funds. The fact that we have to store (or derive) a new one and then keep that secret doesn't seem like a particularly huge burden.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IMO a wallet's state should contain the following:

  • the wallet seed: this is the most important thing to secure to protect against an attacker stealing your funds
  • the channels state: this is important to secure (and back-up) to restore channels on a different device, but an attacker with access to this data cannot steal your funds, as this shouldn't contain private keys or fully signed transactions
  • the payments history: this is not critical and optional to backup, an attacker with access to this learns preimages, but cannot steal funds
  • the contacts list: this is not critical and optional to backup, and shouldn't allow an attacker to steal funds

Of course, all those DBs should be encrypted, but the only highly sensitive entry should be the seed. The others can only be exploited if they're stolen by someone who colludes with your channel peer(s), which is IMO a different security assumption.

If we don't automatically update offers, the contact_secret cannot let attackers steal funds even if it leaks. I don't understand why we'd want to take more risks for a feature that's IMO not useful at all? Offers can be refreshed using BIP 353 or real-world interaction, would we really gain something by automatically updating them here?

blip-0042.md Outdated Show resolved Hide resolved
blip-0042.md Outdated Show resolved Hide resolved
Comment on lines +187 to +191
- If the offer contains `offer_issuer_id`:
- `offer_node_id = offer_issuer_id`.
- Otherwise, the offer must contain `offer_paths`:
- `offer_node_id` is set to the last `blinded_node_id` of the first
`path`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Just say the key used to sign the invoice?

Copy link
Contributor Author

@t-bast t-bast Oct 21, 2024

Choose a reason for hiding this comment

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

That wouldn't cover all cases, because when you have multiple blinded paths and no offer_issuer_id, the key you use to sign the invoice depends on the blinded path that was used for the invoice_request. So if Alice is trying to pay Bob, and Alice has multiple blinded paths in her own offer, she wouldn't know which local key to use in the ECDH (since she's the one sending invoice_request, not receiving it)?

That's why I'm explicitly specifying that when multiple blinded paths are used, we only use the first one for this contact_secret derivation. Does that make sense?

blip-0042.md Show resolved Hide resolved
- Detail normative section to clarify that `contact_secret`s are
  bidirectional and set by the first payer.
- Remove automatic matching of incoming payments using distinct secrets.
- Expand the contact secrets section with UX examples.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants