-
Notifications
You must be signed in to change notification settings - Fork 364
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
BOLT 12 outbound PaymentId
#2468
BOLT 12 outbound PaymentId
#2468
Conversation
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.
Still parsing the payment_id
commit
lightning/src/events/mod.rs
Outdated
@@ -1437,6 +1448,17 @@ impl MaybeReadable for Event { | |||
}; | |||
f() | |||
}, | |||
33u8 => { |
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.
Hmm, should this be even? Seems weird to let an outbound payment silently fail
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.
I guess that would mean a user couldn't downgrade if they have this event serialized. Do I have that right? I don't know the right answer here. If the user needs to downgrade then this could prevent them from doing so.
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.
Indeed, that's what that would mean. I don't have a strong opinion, as long as its in a pending-releasenotes file :)
lightning/src/offers/signer.rs
Outdated
) -> Self { | ||
// Encrypt payment_id | ||
let encrypted_payment_id = payment_id.map(|payment_id| { | ||
PaymentId(expanded_key.crypt_for_offer(payment_id.0, &nonce.0)) |
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.
I think it's not the best practice to reuse nonces, not sure if it matters here though.
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.
@TheBlueMatt Would like your expertise here.
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.
The cardinal rule is "anything you put into a hash function you must always be able to deserialize into a unique input if you were to see the unhashed bytes". An IV (which in most cases is really just an extra input to your hash function) is mostly a way to force yourself to abide by that rule by fixing a few bytes of the input to a specific to a unique input in your code. In this case we really have a second IV (WITH[OUT]_ENCRYPTED_PAYMENT_ID_HMAC_INPUT
) so its all fine.
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.
So there's actually two different uses here.
- For
crypt_for_offer
, the nonce is used as the IV forChaCha20::get_single_block
to encrypt the payment id. - For the
hmac_for_offer
, the nonce is used as one input along with an IV specific to the message type.
For the latter, WITH[OUT]_ENCRYPTED_PAYMENT_ID_HMAC_INPUT
are also provided though it seems to me those might not be strictly necessary given the IV for the message type dictates whether a payment id is included in the input, in practice.
Does that sound right?
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.
Ohoh, the question was about the nonce rather than reusing the IV in two separate contexts (which is what I responded to above). Yea, given the nonce is actually exposed to the sender, using it as the IV for the encryption of the payment id seems like a bad idea :). This may result in the sender being able to detect duplicate payment ids or repeated bits in the payment id.
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.
What's the alternative? And are you defining the sender as the payer? The sender is the one constructing this (payer) metadata for inclusion in an InvoiceRequest
. Do you mean the nonce is exposed to the recipient?
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.
Okay, sorry, I got really confused cause we're talking about multiple nonces + IVs, but the nonce is used as an IV and....
Anyway, so what I was thinking was - what if a user re-uses a payment id (or has some otherwise-structured data in their payment id where a user learning the xor between two unencrypted payment ids is a Bad Thing). In a classic block cipher the IV is XOR'd with the first block to randomize, but its just straight XOR, not hidden at all. In that case, we could start to see an issue here, but that's not how Chacha works, where the IV is really just a second half of the key itself.
In general its bad practice to reuse material, but in this case I'm totally okay with it - its kinda nice to avoid shoving a second 16 byte thing in our metadata and the two uses here are both inputs to something that "looks like" a hash function - sha256 and chacha (which is a sponge), plus given its "public data"...who cares?
lightning/src/offers/signer.rs
Outdated
let mut encrypted_payment_id = [0u8; PaymentId::LENGTH]; | ||
encrypted_payment_id.copy_from_slice(&metadata[..PaymentId::LENGTH]); | ||
|
||
let metadata = &metadata[PaymentId::LENGTH..]; |
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.
This variable shadowing confused me at first, could we name this something else?
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.
Hmm... shadowing actually prevents you from passing the wrong metadata later. Could add a comment stating that the encrypted PaymentId
is sliced off. Made the slicing explicit at each use for now, instead of shadowing. Not sure which you'd prefer.
lightning/src/offers/signer.rs
Outdated
) -> Self { | ||
// Encrypt payment_id | ||
let encrypted_payment_id = payment_id.map(|payment_id| { | ||
PaymentId(expanded_key.crypt_for_offer(payment_id.0, &nonce.0)) |
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.
The cardinal rule is "anything you put into a hash function you must always be able to deserialize into a unique input if you were to see the unhashed bytes". An IV (which in most cases is really just an extra input to your hash function) is mostly a way to force yourself to abide by that rule by fixing a few bytes of the input to a specific to a unique input in your code. In this case we really have a second IV (WITH[OUT]_ENCRYPTED_PAYMENT_ID_HMAC_INPUT
) so its all fine.
lightning/src/events/mod.rs
Outdated
@@ -1437,6 +1448,17 @@ impl MaybeReadable for Event { | |||
}; | |||
f() | |||
}, | |||
33u8 => { |
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.
Indeed, that's what that would mean. I don't have a strong opinion, as long as its in a pending-releasenotes file :)
8958209
to
e0bdae9
Compare
Codecov ReportPatch coverage:
❗ Your organization is not using the GitHub App Integration. As a result you may experience degraded service beginning May 15th. Please install the Github App Integration for your organization. Read more. Additional details and impacted files@@ Coverage Diff @@
## main #2468 +/- ##
==========================================
+ Coverage 90.54% 90.82% +0.27%
==========================================
Files 109 109
Lines 57180 59554 +2374
Branches 57180 59554 +2374
==========================================
+ Hits 51776 54089 +2313
- Misses 5404 5465 +61
☔ View full report in Codecov by Sentry. |
e0bdae9
to
fadf972
Compare
Needs rebase. |
fadf972
to
4ebb061
Compare
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.
Mostly nits, some questions, and only really two or three things.
/// Keys used for signing a [`Bolt12Invoice`] if they can be derived. | ||
/// | ||
/// If `Some`, then must respond with methods that use derived keys. Otherwise, should respond | ||
/// with an invoice signed explicitly. |
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.
"signed explicitly" is pretty confusing - I think we mean "signed by the node's node_id secret key" (or whatever we call it)?
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.
Re-wrote in terms of which method should be called in each situation.
lightning/src/ln/inbound_payment.rs
Outdated
pub(crate) fn crypt_for_offer(&self, mut bytes: [u8; 32], iv_bytes: &[u8; IV_LEN]) -> [u8; 32] { | ||
let chacha_block = ChaCha20::get_single_block(&self.offers_base_key, iv_bytes); | ||
for i in 0..bytes.len() { | ||
bytes[i] = chacha_block[i] ^ bytes[i]; |
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.
nit: this file does the same get_single_block
-then-xor thing a bunch of times, should/can we just create a ChaCha20::encrypt_single_block
? (also it skeeves me out seeing this, yes ChaCha is a stream cipher and xor is how we encrypt, but its weird that the callsite knows that).
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.
Done though needed an in-place version for this new code.
@@ -169,7 +170,7 @@ impl<'a, T: secp256k1::Signing> OfferBuilder<'a, DerivedMetadata, T> { | |||
secp_ctx: &'a Secp256k1<T> | |||
) -> Self where ES::Target: EntropySource { | |||
let nonce = Nonce::from_entropy_source(entropy_source); | |||
let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES); | |||
let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES, None); |
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.
Unrelated to this PR, but is there no way in BOLT12 to use payment_metadata - ie encode something which will come back to the ultimate recipient in the onion? Our users can currently use payment_metadata as a sort of payment_id for inbound payments, but it seems like in BOLT12 they can't?
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.
IIUC, it can go into the BlindedPath
s inside the Bolt12Invoice
. That's how we do stateless inbound payments for offers, at least. We could include additional data there.
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.
Oh, duh, right. Yea, we'll want to make sure our pipeline there lets us include a PaymentMetadata
directly, I think (cc @valentinewallace)
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.
Def, added tracking for this to #1970.
@@ -227,5 +325,5 @@ fn hmac_for_message<'a>( | |||
hmac.input(DERIVED_METADATA_HMAC_INPUT); |
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.
Unrelated to this PR and I cant comment there, but we have a type confusion issue above - we only actually include each TLV's value in the hmac, not the type, we absolutely need to include both.
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.
TlvRecord::record_bytes
contains the type and is included in the hmac above:
for record in tlv_stream {
hmac.input(record.record_bytes);
}
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.
Ah, okay, didnt check the tlv stream, easy to confuse record_bytes
for not including the type :)
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.
lol... #1738 (comment)
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.
Ha, well I agree record_bytes
is better than data
, but I still find it confusing :)
lightning/src/ln/outbound_payment.rs
Outdated
let entry = pending_outbounds.entry(payment_id); | ||
if let hash_map::Entry::Occupied(entry) = &entry { | ||
if let PendingOutboundPayment::AwaitingInvoice { .. } = entry.get() { } else { | ||
return Err(PaymentSendFailure::DuplicatePayment) |
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.
Lets use a separate method for upgrading from AwaitingInvoice to Retryable, I dont want someone to call send_payment with a BOLT11 after they send a BOLT12 and double-pay somehow.
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.
Could you describe the scenario in more detail? Note that there is a send_payment_for_bolt12_invoice
method added in the next PR.
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.
If we call add_new_awaiting_invoice
(via some pub ChannelManager::send_bolt12_payment
), we get a PendingOutboundPayment::AwaitingInvoice
-state payment. Now, if the user calls send_payment
with a BOLT11 payment with the same payment id, add_new_pending_payment
will overwrite our previous payment, thinking we've received the BOLT12 invoice. While I don't know why someone would do this, its definitely super unexpected behavior given the payment ids are explicitly an "idempotency token", and here we're not being idempotent.
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.
Ah, I see... Unfortunately, while trying to reuse send_payment_internal
, add_new_pending_payment
would be called in both cases, so this will need some refactoring unless we are ok duplicating code.
It would be nice to perform the check upfront such that the code can be reused still without needing to do something like pipe a is_bol12
parameter through the helpers. But even so, it seems we don't hold the pending_outbound_payments
lock throughout these calls. So there's probably edge cases where the check is valid initially but may not be valid later if the locks isn't held throughout.
Would it make sense to hold on to the locks and pass a hash map entry (either occupied or vacant) to the helper functions? Or is there a reason why we drop and re-acquire the lock throughout these helpers? (cc: @valentinewallace )
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.
Unfortunately, while trying to reuse send_payment_internal, add_new_pending_payment would be called in both cases, so this will need some refactoring unless we are ok duplicating code.
I'm not convinced we should be calling through send_payment_internal - that method is set up to return early with a RetryableSendFailure
for immediate return to the user, but at this point we're handling a bolt12 invoice and don't want to return immediately. Rather, retry_payment_internal
is set to abandon_payment
, creating a PaymentFailed
event on failure and not returning something we'd have to figure out how to handle.
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.
Ah, that makes sense. Ok, reverted but will be in the next PR as per #2468 (comment).
4ebb061
to
3443e5e
Compare
3443e5e
to
840729d
Compare
I think I'm happy with this, feel free to squash IMO. |
840729d
to
c805fc8
Compare
c805fc8
to
098ad57
Compare
/// If `Some`, must call [`respond_using_derived_keys`] when responding. Otherwise, call | ||
/// [`respond_with`]. |
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.
Could we do something similar to SigningPubkeyStrategy
to only expose the right method for the given verified invreq?
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.
Not possible as this involves a dynamic check on the metadata. Ultimately, it comes from bytes that need to be parsed opposed to using statically constructed objects.
@@ -556,6 +579,63 @@ impl InvoiceRequest { | |||
InvoiceBuilder::for_offer(self, payment_paths, created_at, payment_hash) | |||
} | |||
|
|||
/// Verifies that the request was for an offer created using the given key. Returns the verified |
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.
Can't comment on the right line, but it seems like if the offer was created using derived keys then users should not directly call InvReq::respond_with
and should instead call verify
first. If that's the case, I think InvReq::respond_with
could use a docs note.
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.
They could try to call it, but they would have a hard time signing the invoice. 😛 Added some more docs.
InvoiceRequest::verify_and_respond_using_derived_keys takes a payment hash. To avoid generating one for invoice requests that ultimately cannot be verified, split the method into one for verifying and another for responding.
Metadata such as the PaymentId should be encrypted when included in an InvoiceRequest or a Refund, as it is user data and is exposed to the payment recipient. Add an encryption key to ExpandedKey for this purpose instead of reusing offers_base_key.
This hides an encryption implementation detail from callers.
Similar to ChaCha20::encrypt_single_block only encrypts in-place.
When receiving a BOLT 12 invoice originating from either an invoice request or a refund, the invoice should only be paid once. To accomplish this, require that the invoice includes an encrypted payment id in the payer metadata. This allows ChannelManager to track a payment when requesting but prior to receiving the invoice. Thus, it can determine if the invoice has already been paid.
098ad57
to
7a3e06b
Compare
When receiving a BOLT 12 invoice originating from either an invoice request or a refund, the invoice should only be paid once. To accomplish this, require that the invoice includes an encrypted payment id in the payer metadata. This allows
ChannelManager
to track a payment when requesting but prior to receiving the invoice. Thus, it can determine ifthe invoice has already been paid.
PaymentId
in payer metadata (forInvoiceRequest
andRefund
) to track an expected paymentAdds anAwaitingInvoice
variant toPendingOutboundPayment
to track whether anInvoice
is expectedAdds aInvoiceRequestFailed
event to notify users of a request that didn't get a response or was unexpectedBased on #2432.