-
Notifications
You must be signed in to change notification settings - Fork 427
Support signing BOLT 12 messages in NodeSigner
#2432
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
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
| } | ||
|
|
||
| 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 |
valentinewallace
left a 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.
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 |
devrandom
left a 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.
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::newwhich let's the user set anymetadata,OfferBuilder::deriving_signing_pubkeywithout callingOfferBuilder::path, andOfferBuilder::deriving_signing_pubkeywith callingOfferBuilder::pathto 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 Offers 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_pubkeyto an ephemeral pubkey and allowChannelManagerto derive the corresponding secret key from theOffer::metadata. Thus, for this way,NodeSigner::sign_bolt12_messageis not even called. Instead,ChannelManagerwill 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
|
|
||
| /// 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
invoicewhich 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 theinvoiceonce we get it before sending the HTLC, and notably means VLS won't be involved in our metadata orinvoice_requestsigning at all. - For inbound payments/
invoicesigning, 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 Messages 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.
| /// | ||
| /// 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
| 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
TheBlueMatt
left a 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.
LGTM, feel free to squash IMO.
lightning/src/offers/invoice.rs
Outdated
| } | ||
| } | ||
|
|
||
| // 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?
| } | ||
| } | ||
|
|
||
| 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 :/
TheBlueMatt
left a 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.
Definitely agree it'd be nice to use the {offer,refund,invoice_request}_accessors in {Unsigned,}Invoice.
| 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:
InvoiceRequestafter scanning anOffer,Invoiceafter scanning aRefund, andInvoicewhen handling anInvoiceRequest.Extend the
NodeSignertrait to support signing BOLT 12 messages such that it can be used in these contexts. The method could be used then in anOffersMessageHandlerwhen keys aren't derived from the offer or payer metadata.Taken from #2371.