-
Notifications
You must be signed in to change notification settings - Fork 381
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
Support signing BOLT 12 messages in NodeSigner
#2432
Support signing BOLT 12 messages in NodeSigner
#2432
Conversation
This doesn't build for me on 1.58. Mind rebasing/fixing CI? Feel free to squash too IMO. |
lightning/src/offers/merkle.rs
Outdated
|
||
impl<F, E> SignFunction<E> for F where F: FnOnce(&TaggedBytes, &[u8]) -> Result<Signature, E> {} | ||
|
||
/// Bytes associated with a tag, which are used to produced a [`Message`] digest to sign. |
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.
Does the tag indicate what offers message is being signed? Would appreciate a docs clarification
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.
also, would be good to have a short comment about the format of bytes
(even if it can be deduced by the reader from the BOLT)
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.
Renamed this to TaggedHash
as that is more accurate and re-wrote the docs.
lightning/src/util/test_utils.rs
Outdated
@@ -795,6 +797,12 @@ impl NodeSigner for TestNodeSigner { | |||
unreachable!() | |||
} | |||
|
|||
fn sign_bolt12_message( | |||
&self, _message: &TaggedBytes, _metadata: &[u8] |
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.
@devrandom @ksedgwic any feedback on this API?
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.
isn't the metadata also tagged? would be good in general to have more specific comments about the format
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.
BTW, the _metadata
arg can stay if it provides performance advantages in some applications. validating signers can just ignore 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.
This is what we use for CLN:
/// SignBolt12
#[derive(SerBolt, Debug, Serialize, Deserialize)]
#[message_id(25)]
pub struct SignBolt12 {
pub message_name: WireString,
pub field_name: WireString,
pub merkle_root: Sha256,
pub public_tweak: Octets,
}
///
#[derive(SerBolt, Debug, Serialize, Deserialize)]
#[message_id(125)]
pub struct SignBolt12Reply {
pub signature: Signature,
}
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.
isn't the metadata also tagged? would be good in general to have more specific comments about the format
The metadata was included simply to provide a way to derive the signing keys from the metadata. However, after the discussion in #1989 (comment), it's unclear if this is actually needed.
As currently implemented, LDK's ChannelManager
will either sign the invoice from derived keys for you or delegate to the NodeSigner
. The latter is only reached when the originating offer did not include any blinded paths and thus included the actual node_id
for the signing_pubkey
in the offer. See:
rust-lightning/lightning/src/offers/offer.rs
Lines 158 to 159 in 685f266
/// recipient privacy by using a different signing pubkey for each offer. Otherwise, the | |
/// provided `node_id` is used for the signing pubkey. |
This is what we use for CLN:
/// SignBolt12 #[derive(SerBolt, Debug, Serialize, Deserialize)] #[message_id(25)] pub struct SignBolt12 { pub message_name: WireString, pub field_name: WireString, pub merkle_root: Sha256, pub public_tweak: Octets, } /// #[derive(SerBolt, Debug, Serialize, Deserialize)] #[message_id(125)] pub struct SignBolt12Reply { pub signature: Signature, }
Does this mean you are blindly signing the tagged hash of the merkle root without computing the merkle root from the invoice
or invoice_request
TLV stream on your own?
Where does the tweak come from? Is it CLN-specific?
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.
Apologies for the delay ...
I think we need the full invoice and invoice_request content. Our current interface is missing this.
Need to spend some time thinking about which things are important to enforce though, I'm not up to speed yet ...
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 bytes are now exposed in TaggedHash
.
1aeee45
to
f6aa143
Compare
Build should be fixed now in the latest push. Appears newer versions of rustc did a better job at inferring the types of closure parameters. |
f6aa143
to
b7e1a0a
Compare
FYI, I'll need to rebase this and dependent PRs soon as there are some merge conflicts. |
b7e1a0a
to
08664c3
Compare
Rebased now which included the |
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.
Basically LGTM if the VLS folks are happy
@devrandom IIUC, you don't need the @TheBlueMatt Guessing we can drop it and add it later if needed as the |
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.
looks good from VLS point of view
lightning/src/offers/merkle.rs
Outdated
|
||
let pubkey = pubkey.into(); | ||
let secp_ctx = Secp256k1::verification_only(); | ||
secp_ctx.verify_schnorr(&signature, &digest, &pubkey).map_err(|e| SignError::Verification(e))?; | ||
secp_ctx.verify_schnorr(&signature, &message.to_digest(), &pubkey) |
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.
why verify right after signing? isn't this infallible?
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.
Only if the passed pubkey
, which comes from the message`, corresponds to the key used by the signing function, which is user-supplied.
lightning/src/offers/merkle.rs
Outdated
/// [BIP 340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki | ||
/// [BOLT 12]: https://github.com/rustyrussell/lightning-rfc/blob/guilt/offers/12-offer-encoding.md#signature-calculation | ||
/// Bytes associated with a tag, which are used to produced a [`Message`] digest to sign. | ||
pub struct TaggedHash<'a> { |
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: it's slightly unintuitive that this is called a "hash", but it actually contains the full stream. and in fact, the signer would look at the stream to validate. perhaps it makes sense to call it something like TaggedHashTlvStream
or TlvStreamWithTaggedHash
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.
Out of curiosity, how would the signer validate the stream? Presumably they may want to parse the message and do some checks. But when computing the hash, would calling TaggedHash::to_message
be sufficient given this would be running on their process? Or would the signer not trust this code and opt to reimplement the hash computation?
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 you mean to_digest
. no need to reimplement anything, a code review is enough to trust code in dependencies, so there would be no problem calling that code.
if you are asking what further validation rules we could have on the actual data - we could have some sanity checks on the invoice fields (e.g. maximum amount). but we would also like to handle the payment preimages in the future, so that an attacker can't redirect incoming payments to an intermediate hop. so we would check that the payment hash in the invoice is for a preimage we control
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 you mean
to_digest
. no need to reimplement anything, a code review is enough to trust code in dependencies, so there would be no problem calling that code.
Alright, so my argument for the name is that it's the input to the signing function, and the hash is what is being signed. The fact that the bytes that the hash were computed from is needed for some users is secondary.
if you are asking what further validation rules we could have on the actual data - we could have some sanity checks on the invoice fields (e.g. maximum amount). but we would also like to handle the payment preimages in the future, so that an attacker can't redirect incoming payments to an intermediate hop. so we would check that the payment hash in the invoice is for a preimage we control
Hmm... so to reiterate what I said earlier, NodeSigner::sign_bolt12_message
isn't actually called in the normal, privacy-preserving case. There's essentially three ways we allow creating an Offer
:
OfferBuilder::new
which let's the user set anymetadata
,OfferBuilder::deriving_signing_pubkey
without callingOfferBuilder::path
, andOfferBuilder::deriving_signing_pubkey
with callingOfferBuilder::path
to add blinded paths.
Any InvoiceRequest
messages created in the first case will be rejected by the ChannelManager
.
The last two cases will set Offer::metadata
such that the ChannelManager
implementation of OffersMessageHandler
can verify that any InvoiceRequest
came from one of our Offer
s before replying with an Invoice
.
Further, the last case will set Offer::signing_pubkey
to an ephemeral pubkey and allow ChannelManager
to derive the corresponding secret key from the Offer::metadata
. Thus, for this way, NodeSigner::sign_bolt12_message
is not even called. Instead, ChannelManager
will sign the invoice by deriving the keys from the metadata.
It's only the second case where NodeSigner::sign_bolt12_message
is invoked. See PR #2371 for details. So it seems you'll need some other way to validate this data. Either by implementing OffersMessageHandler
yourself or having ChannelManager
call some other method on NodeSigner
specifically for validation.
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.
Further, the last case will set
Offer::signing_pubkey
to an ephemeral pubkey and allowChannelManager
to derive the corresponding secret key from theOffer::metadata
. Thus, for this way,NodeSigner::sign_bolt12_message
is not even called. Instead,ChannelManager
will sign the invoice by deriving the keys from the metadata.
Sorry for the slow reply and not fully understanding your first comment. But I must be still missing something, since this seems potentially problematic. How will a relying party know that this offer was securely issued by our node if the offer is not bound to our node key? or looking at it from a different direction, why would something outside of the signer be able to enter into an agreement that could in general cause financial loss?
It's a design goal of the signer abstraction that nothing outside the signer can lose money, either by moving assets or by signing bogus invoices and such.
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.
why do we think this is a common case? if, for example, a user wants to pay some bill or buy some merchandise online, they would know what the node_id is (and hopefully allowlist the node_id to reduce having to approve things all the time).
Because its required for recipient privacy - while we could define some static "recipient payment key" which signs all BOLT12 structs, LDK seeks to encourage users to randomize so that offers aren't linkable across payments. For users on eg a mobile device seeking to receive regularly, this is a super nice privacy property. If a sender wants to cache a recipient to pay them regularly, we should consider a way for them to get an amount-less offer which they can pay regularly, possibly via the "do you know the secret key to the pubkey I'm thinking about" protocol Bitcoin Core folks have talked about using for P2P auth.
that said, I'm not sure how useful the offer is, given that it's unsigned and could be arbitrarily generated by anybody.
An invoice is signed by the pubkey which is in the offer, and the invoice bytes start with the full bytes of the offer, so you can validate that an invoice came from a given offer.
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 see. in this case, we should have the approval UX pre-approve an offer for repeated payments. so we should use either the node_id or the offer_id as the "destination".
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.
Note, the spec no longer has an offer_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.
I kinda assumed we'd just have some call on the signer to say "hey, here's an invoice, we're paying it, FYI" and be done with it. Presumably any VLS client will pass VLS any offers which the client has authorized to pay via some secure channel (presumably also how it works today with BOLT11) or has some policy enforcement. So all LDK really needs to do is give VLS a mapping from the offer to the invoice that we're about to pay, and VLS can validate that the invoice is built using the offer.
Can't VLS use the metadata to verify an invoice instead of needing the client to pass it authorized offers? Presumably, it has the same key material as the NodeSigner
and thus can form an ExpandedKey
to verify. Or am I misunderstanding the setup.
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 VLS use the metadata to verify an invoice instead of needing the client to pass it authorized offers? Presumably, it has the same key material as the NodeSigner and thus can form an ExpandedKey to verify. Or am I misunderstanding the setup.
We could move all the metadata generation and signing into the signer, but I'm not sure that would fix this - ultimately the security of VLS depends on some kind of secure channel between VLS and the user (bypassing the untrusted "node") for the user to tell VLS they intend to pay something (BOLT11 invoice/BOLT12 offer/BOLT12 offer repeatedly up to some amount). Once we have that, there's not a lot of reason (IMO) to have VLS do that via metadata verification vs just storing the full BOLT12 offer.
Isn't the metadata also included in the TLV stream? Seems like another reason to exclude it as an explicit parameter for now, since presumably signers are already parsing the TLV stream. |
08664c3
to
9403d31
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 #2432 +/- ##
==========================================
+ Coverage 90.33% 91.66% +1.33%
==========================================
Files 106 106
Lines 55732 68959 +13227
Branches 55732 68959 +13227
==========================================
+ Hits 50347 63214 +12867
- Misses 5385 5745 +360
☔ View full report in Codecov by Sentry. |
9403d31
to
da70db6
Compare
Yeah, went ahead and removed the parameter. |
lightning/src/sign/mod.rs
Outdated
@@ -620,6 +621,15 @@ pub trait NodeSigner { | |||
/// Errors if the [`Recipient`] variant is not supported by the implementation. | |||
fn sign_invoice(&self, hrp_bytes: &[u8], invoice_data: &[u5], recipient: Recipient) -> Result<RecoverableSignature, ()>; | |||
|
|||
/// Signs a BOLT 12 message. | |||
/// | |||
/// See [`SignFunction`] for details. |
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 isn't a super useful documentation, SignFunction
also doesn't really say anything but "read TaggedHash
" which only kinda says something about its contents.
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.
Removed the SignFunction
commit in the latest push now that merkle::sign_message
only takes one parameter. Plus, older rustc
were having a hard time inferring the parameter types in closures. Re-wrote the docs here and in merkle::sign_message
.
lightning/src/offers/merkle.rs
Outdated
/// | ||
/// [`invoice::SIGNATURE_TAG`]: crate::offers::invoice::SIGNATURE_TAG | ||
/// [`invoice_request::SIGNATURE_TAG`]: crate::offers::invoice_request::SIGNATURE_TAG | ||
pub tag: &'a str, |
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.
Should this be an enum if there's only 2-3 valid tags?
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 is now only a parameter to a pub(super)
constructor, so leaving it as &str
.
lightning/src/offers/merkle.rs
Outdated
pub tlv_stream: &'a [u8], | ||
|
||
/// The cached digest to sign. | ||
digest: RefCell<Option<Message>>, |
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 isn't safe since the tlv_stream
and tag
are pub, which would allow a user to change them then call to_digest
and get a cached, now invalid, result.
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. Good catch... should have used an accessor. The latest push removes the cache and instead makes TaggedHash
simply a wrapper around Message
.
lightning/src/offers/merkle.rs
Outdated
/// [BIP 340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki | ||
/// [BOLT 12]: https://github.com/rustyrussell/lightning-rfc/blob/guilt/offers/12-offer-encoding.md#signature-calculation | ||
/// Bytes associated with a tag, which are used to produced a [`Message`] digest to sign. | ||
pub struct TaggedHash<'a> { |
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.
Talking more abstractly about security model for VLS in BOLT12:
- For outbound payments, I feel like VLS should verify the HTLC we're sending by just receiving an
invoice
which it can detect matches up with a user-providedoffer
(and potentially amount) which it has listed as to-be-paid. This requires ~nothing from LDK aside from us giving them theinvoice
once we get it before sending the HTLC, and notably means VLS won't be involved in our metadata orinvoice_request
signing at all. - For inbound payments/
invoice
signing, VLS needs to be more involved as it wants to ensure we aren't signing invoices with bogus amounts for the expected payment, at least for those doing the non-private payments, as Jeff notes. Here too, I don't think VLS needs to be involved in the metadata (modern equivalent of payment_secret) generation/validation. However, in the non-private offer case, VLS/the signer wants to verify the BOLT12 offer/invoice data, at least the amount and description fields. In that case, I don't think it makes sense to make the API read like you're only blindly signing a hash, but rather make it clear that there's data there you may want to validate.
da70db6
to
ac6ca72
Compare
/// | ||
/// [BIP 340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki | ||
/// [BOLT 12]: https://github.com/rustyrussell/lightning-rfc/blob/guilt/offers/12-offer-encoding.md#signature-calculation | ||
pub struct TaggedHash(Message); |
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 we still need this struct? It seems like a very thin wrapper
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.
Yeah, as it ensures that Message
is constructed properly.
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.
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.
No, it's exactly as you say. We only want to pass Message
s to sign_message
that have been constructed via message_digest
. That's the typical use of a wrapper type. Having message_digest
directly return a TaggedHash
would be suitable, too, I suppose.
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, okay. I see that this guarantees that the Message
was constructed properly in UnsignedInvoice{Request}::new
. I may be missing something, this seems like a lot for an internal guarantee, but I see that reasoning.
/// Signs the invoice using the given function. | ||
/// | ||
/// This is not exported to bindings users as functions aren't currently mapped. | ||
pub fn sign<F, E>(self, sign: F) -> Result<Bolt12Invoice, SignError<E>> | ||
pub fn sign<F, E>(mut self, sign: F) -> Result<Bolt12Invoice, SignError<E>> |
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.
Why can't this take a NodeSigner
? In #2371 the closure just calls the new NodeSigner
method.
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.
Hmmm.. I'd rather not limit people to using NodeSigner
if they are using offers
as a standalone module.
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.
Hm, with a NodeSigner
it could be a standalone module for non-Rust users as well, but 🤷♀️
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 sure I follow what you mean 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.
Just that this method isn't exposed to bindings users, and people using offers as a standalone module are free to create a custom NodeSigner
implementation.
lightning/src/offers/invoice.rs
Outdated
@@ -355,62 +360,82 @@ impl<'a> InvoiceBuilder<'a, DerivedSigningPubkey> { | |||
} | |||
|
|||
let InvoiceBuilder { invreq_bytes, invoice, keys, .. } = self; | |||
let unsigned_invoice = UnsignedBolt12Invoice { invreq_bytes, invoice }; | |||
let unsigned_invoice = UnsignedBolt12Invoice::new(invreq_bytes, invoice); | |||
|
|||
let keys = keys.unwrap(); |
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 add a comment on this unwrap
? Hard to tell at a glance how it's safe
I guess it's fine due to the concrete type DerivedSigningPubkey
actually
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.
Got rid of the unwrap
by having DerivedSigningPubkey
wrap KeyPair
. It's a bit more difficult to do something similar for InvoiceRequestBuilder
, unfortunately, since build_without_checks
handles both parameterizations.
Hmm... maybe, instead of us expanding |
I kinda assumed the app developer would prefer a really simple API where they scan a QR code (BOLT12 offer) and then pass it to VLS "blessing it" and then pass it to LDK and be done with it. If we go the route where the user gets the invoice they'll have to deal with a bunch of onion message handling, as @jkczyz noted above. |
OK, so in order to streamline the API, LDK should provide invoices to VLS. Just to be more precise about what VLS needs to validate:
So we definitely need to see the invoice, even if there's a mechanism to bless an offer. I'll follow up on @jkczyz's thoughts about how to do that. |
That seems OK. I couldn't find where chanman implements this interface though in either this PR or in |
It's in #2371. Though @TheBlueMatt noted offline that this would require VLS users to use two parameterizations, which may not be desirable. |
cce0cc9
to
63e0174
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.
LGTM, feel free to squash IMO.
lightning/src/offers/invoice.rs
Outdated
@@ -425,6 +425,7 @@ impl UnsignedBolt12Invoice { | |||
} | |||
} | |||
|
|||
// Allows passing a function that takes `UnsignedBolt12Invoice` to `merkle::sign_message`. |
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: In general, docs on trait impls don't really show up well on docs.rs. Its kinda nice to mention these things in the struct docs so that they're visible (or in this case the sign_message
docs). Not a huge deal in this case tho.
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.
Moved to sign_message
.
let pubkey = pubkey.into(); | ||
let secp_ctx = Secp256k1::verification_only(); | ||
secp_ctx.verify_schnorr(&signature, &digest, &pubkey).map_err(|e| SignError::Verification(e))?; | ||
secp_ctx.verify_schnorr(&signature, digest, &pubkey).map_err(|e| SignError::Verification(e))?; |
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 you remind me why we need to verify here? Its pretty nontrivial additional CPU cost in a method (presumably) reachable directly as a message handler that an attacker can hammer as much as they want.
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.
It's for the case when the users signs but doesn't use the privkey corresponding to the given pubkey. We can make this a debug assertion if you prefer. Just need to rip out SignError
since we wouldn't need two variants.
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.
We can do it in a followup, but, yea, I don't think we need to be that careful - if users screw up then the user sees an error...somewhere else. As long as debug assertions hit it I don't think its worth it?
@@ -730,6 +1024,25 @@ impl Writeable for InvoiceContents { | |||
} | |||
} | |||
|
|||
impl TryFrom<Vec<u8>> for UnsignedBolt12Invoice { |
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, what do we think about unknown fields here? Its kinda weird for an external signer to sign off on even an odd field that it doesn't understand, even if this will fail for even fields. For now we can document this on the struct definition/signer methods, I guess, but its something to consider....just not sure how.
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.
Right, our deserialization code simply skips those records. If gave back some Vec
of unknown TLV records, we could at least expose that in the interface. Added docs for now.
LGTM after Matt's feedback is addressed. |
63e0174
to
6805acb
Compare
Feel free to squash fixups. |
The function used to sign BOLT 12 messages only takes a message digest. This doesn't allow signers to independently verify the message before signing nor does it allow them to derive the necessary signing keys, if needed. Introduce a TaggedHash wrapper for a message digest, which each unsigned BOLT 12 message type constructs upon initialization. Change the signing function to take AsRef<TaggedHash>, which each unsigned type implements. This allows the signing function to take any unsigned message and obtain its tagged hash.
InvoiceBuilder is parameterized by a SigningPubkeyStrategy, either ExplicitSigningPubkey and DerivedSigningPubkey. It also holds an Option<KeyPair>, which may be None and Some for those strategies, respectively. This leads to methods for InvoiceBuilder parameterized by DerivedSigningPubkey needing to blindly unwrap the Option<KeyPair>. Instead, have DerivedSigningPubkey wrap KeyPair.
Using `contents` for the field name is more consistent with the signed messages.
InvoiceRequest wraps OfferContents, which shouldn't be exposed as it is an implementation detail. Define a macro for Offer accessor methods so that InvoiceRequest and UnsignedInvoiceRequest can also define them.
Various messages wrap InvoiceRequestContents, which shouldn't be exposed as it is an implementation detail. Define a macro for InvoiceRequest accessor methods so that these messages can also define them.
Also, expose both Offer and InvoiceRequest functions in UnsignedInvoiceRequest.
Bolt12Invoice can either be for an Offer (via an InvoiceRequest) or a Refund. It wraps those types, so expose their methods on both Bolt12Invoice and UnsignedBolt12Invoice. Since Refund does not have all the Offer/InvoiceRequest methods, use an Option return type such that None can returned for refund-based invoices. For methods that are duplicated between Offer/InvoiceRequest and Bolt12Invoice, prefer the (non-Option, if applicable) method from Bolt12Invoice (e.g., amount_msats, signing_pubkey).
An earlier commit introduced TaggedHash for use in sign_message. For consistency, use it in verify_signature, too.
BOLT 12 messages need to be signed in the following scenarios: - constructing an InvoiceRequest after scanning an Offer, - constructing an Invoice after scanning a Refund, and - constructing an Invoice when handling an InvoiceRequest. Extend the NodeSigner trait to support signing BOLT 12 invoices such that it can be used in the latter contexts. The method could be used in an OffersMessageHandler.
6805acb
to
39012e3
Compare
} | ||
|
||
impl Bolt12Invoice { | ||
invoice_accessors!(self, self.contents); |
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 added a parameter for Offer
vs Refund
to the {Unsigned}Bolt12Invoice
type signatures, could we use {offer,refund}_accessors
macros for each respective variant and avoid all the boilerplate-y enum-matching methods below? I suspect there might be issues but thought I'd ask.
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 not. Refund
doesn't contain the same fields as Offer
and includes InvoiceRequest
fields. There are some Option
mismatches, too.
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... actually let me think a bit more on this... I suspect it would sorta be done but the real problem is overlapping methods from each. I guess a prefix may work, but bindings might be gross.
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 bindings see through macros just fine, not sure what else would be gross.
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 mean having a parameterized type where one set of methods is defined for one parameterization and another set is defined for another parameterization.
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, yea, that may be annoying. I guess there's not really a great way to do this :/
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.
Definitely agree it'd be nice to use the {offer,refund,invoice_request}_accessors
in {Unsigned,}Invoice
.
|
||
// Append the signature TLV record to the bytes. | ||
let signature_tlv_stream = SignatureTlvStreamRef { | ||
signature: Some(&signature), | ||
}; | ||
signature_tlv_stream.write(&mut bytes).unwrap(); | ||
signature_tlv_stream.write(&mut self.bytes).unwrap(); |
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 is broken if the invoice_request or invoice or offer have any TLVs above 1000, right? Are those allowed/removed somehow? Its not a regression, so doesn't need fixing 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.
Each TLV type will only read in its allowed range. And ParsedMessage
ensures there are no more TLV records left.
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.
Right, guess I just wasn't sure if we had any support for 1k+ TLVs anywhere.
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, no. I believe custom TLVs need to be in their respective message type ranges.
For offer
:
- if the offer contains any TLV fields greater or equal to 80:
- MUST NOT respond to the offer.
For invoice_request
:
- MUST fail the request if any non-signature TLV fields greater or equal to 160.
Nothing for invoice
, but I'll leave a comment on the spec PR.
} | ||
|
||
impl Bolt12Invoice { | ||
invoice_accessors!(self, self.contents); |
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 bindings see through macros just fine, not sure what else would be gross.
BOLT 12 messages need to be signed in the following scenarios:
InvoiceRequest
after scanning anOffer
,Invoice
after scanning aRefund
, andInvoice
when handling anInvoiceRequest
.Extend the
NodeSigner
trait to support signing BOLT 12 messages such that it can be used in these contexts. The method could be used then in anOffersMessageHandler
when keys aren't derived from the offer or payer metadata.Taken from #2371.