bolt12: add string-codec wrappers and fuzz harnesses - #11146
Conversation
🟠 PR Severity: HIGH
🟡 Medium (10 files, bumped to HIGH)
🟢 Low (10 files, tests/fuzz/vectors, excluded from bump calculation)
AnalysisThe To override, add a |
30f2828 to
90fc244
Compare
ViktorT-11
left a comment
There was a problem hiding this comment.
Did a first pass of this PR, and in general I think this looks great. I don't have that much useful feedback to provide, and think this should be pretty much ready to go once its dependant PR has been merged.
Potentially the fuzzing commit could be broken out into a separate PR as well.
| // The gap this closes: a validly Alice-signed invoice passes the | ||
| // reader, so only validateInvoiceNodeID separates it from a genuine | ||
| // one. | ||
| inv := validInvoice(t) | ||
| inv.InvoiceNodeID = tlv.SomeRecordT( | ||
| tlv.NewPrimitiveRecord[tlv.TlvType176](alicePub), | ||
| ) | ||
| sig, err := SignInvoice(inv, alicePriv) | ||
| require.NoError(t, err) | ||
| inv.Signature = tlv.SomeRecordT( | ||
| tlv.NewPrimitiveRecord[tlv.TlvType240](sig), | ||
| ) | ||
|
|
||
| ir.InvreqPayerID = tlv.SomeRecordT( | ||
| tlv.NewPrimitiveRecord[tlv.TlvType88](privKey.PubKey()), | ||
| require.NoError(t, ValidateInvoiceRead( | ||
| inv, bitcoinMainnetGenesisHash, InvoiceFeatureCatalogues{}, | ||
| )) | ||
| require.ErrorIs( | ||
| t, validateInvoiceNodeID(inv, bobPub), | ||
| ErrInvoicePathNodeIDMismatch, |
There was a problem hiding this comment.
I think this case here is better suited for the TestValidateInvoiceForPayment test. However, as this already covered in that test to some extent, it's IMO more confusing to include it here than to just remove this part from the TestValidateInvoiceNodeID test.
There was a problem hiding this comment.
I shifted it into TestValidateInvoiceForPayment
| // representation (lni1...). The spec reader gates (chain, features, signature) | ||
| // are folded in via ValidateInvoiceRead, and the expiry gate is enforced via | ||
| // ValidateInvoiceExpiry. | ||
| func DecodeInvoiceString(s string, now time.Time, |
There was a problem hiding this comment.
Building on top of the commit prior to this one: Could it potentially also be useful to also export a "DecodeInvoiceStringForPayment" function that decodes an invoice string, but which additionally also takes an invoice request string param, and then calls the ValidateInvoiceForPayment function?
Given that this exported function will be vulnerable to the issue described in the previous commit, in case it's the response from an invoice request.
There was a problem hiding this comment.
I would postpone this until we have specific call sites. This helper is only intended for invoices that have been received out of band (which may not have an invoice request?). The usual bolt12 branch only receives an invoice embedded in an onion message in tlv-byte form, which then is decoded and validated via ValidateInvoiceForPayment, which could be called after this. But I see the problem that this could be forgotten.
There was a problem hiding this comment.
Gotcha :)! Yeah I think what makes it harder for me to determine what's useful and not is that at the state of this PR the ValidateInvoiceForPayment function is never used outside of tests :).
GeorgeTsagk
left a comment
There was a problem hiding this comment.
The PR description does not mention the ValidateInvoiceForPayment commit. Worth adding a line since it is the new exported payer entry point.
| return VerifyInvoice(inv) | ||
| } | ||
|
|
||
| // ValidateInvoiceForPayment runs the full set of payer-side invoice checks in |
There was a problem hiding this comment.
This runs everything except the expiry gate, so the full set claim is off. ValidateInvoiceRead skips expiry by design and only DecodeInvoiceString folds ValidateInvoiceExpiry in. In the main payment flow invoices arrive as raw TLV over onion messages, not as lni strings, so a caller trusting this composite never hits the expiry MUST and can pay an expired invoice. Same argument as for the path binding: take a now time.Time and fold ValidateInvoiceExpiry in so the caller cannot forget it.
There was a problem hiding this comment.
Right, good call, added that.
| return nil | ||
| } | ||
|
|
||
| return validateInvoiceNodeID(inv, finalBlindedNodeID) |
There was a problem hiding this comment.
Doc suggestion only, behavior is fine. An offerless request lands here too, where the spec says MAY reject if invoice_node_id cannot be confirmed out-of-band. Rejecting fail-closed is stricter than the spec and that is good, but the doc and param name only speak of blinded paths. Worth a sentence that an offerless caller passes the node id it confirmed out-of-band.
There was a problem hiding this comment.
That's true, I reworded the parameter to be a bit more neutral and updated the function's docstring to note what one should pass in.
90fc244 to
b359c2b
Compare
🟡 PR Severity: MEDIUM
🟡 Medium (5 files)
🟢 Low (8 files)
AnalysisThe To override, add a |
| // cannot forget any of them. expectedNodeID is only consulted when | ||
| // offer_issuer_id is absent. Otherwise the expected signer is derived from | ||
| // the request. |
There was a problem hiding this comment.
Nit: The doc mentions: "expectedNodeID is only consulted when offer_issuer_id is absent.", but this function then tests that it's still consulted after this comment in the test:
// The check is unconditional, so a wrong expectation is caught even
// though offer_issuer_id is present and the readers already bound it.
So I think this is a stale comment which should be removed?
There was a problem hiding this comment.
Improved the comments around this.
| // cannot forget any of them. expectedNodeID is only consulted when | ||
| // offer_issuer_id is absent. Otherwise the expected signer is derived from | ||
| // the request. | ||
| func TestValidateInvoiceForPayment(t *testing.T) { |
There was a problem hiding this comment.
nit: Could also include test coverage for when there's a missmatch in the amount between the invoice and the invoice request, hence failing the ValidateInvoiceAgainstRequest part of the ValidateInvoiceForPayment function.
There was a problem hiding this comment.
Good idea, addressed!
| // representation (lni1...). The spec reader gates (chain, features, signature) | ||
| // are folded in via ValidateInvoiceRead, and the expiry gate is enforced via | ||
| // ValidateInvoiceExpiry. | ||
| func DecodeInvoiceString(s string, now time.Time, |
There was a problem hiding this comment.
Gotcha :)! Yeah I think what makes it harder for me to determine what's useful and not is that at the state of this PR the ValidateInvoiceForPayment function is never used outside of tests :).
Bundle the payer-side invoice checks into one call so the sender flow cannot forget any of them: the reader gates, the expiry gate, the byte-for-byte mirror match against the originating request, and the binding of invoice_node_id to the node the payer expected to answer.
Decode/EncodeOfferString, Decode/EncodeInvoiceRequestString, and Decode/EncodeInvoiceString compose bech32, the per-message TLV codec, and the previously-landed validators into a single reader/writer API that consumers should use. Reader gates run against Bolt12Features, the package's known feature-bit set, defined in this commit. The string codecs that emit a wire-form message also gate Signature validity, since pre-sign Encode (used to compute the Merkle root) is permitted on (*X).Encode but not at the bech32 boundary.
The bolt12 codec parses bytes that arrive untrusted inside onion messages, so every decoder gets a property-based check that no input pattern drives a panic or breaks the encode/decode bijection. A further harness asserts merkleRoot is a pure function of its record set, since non-determinism there would silently break signature reproducibility. Wiring bolt12 into FUZZPKG makes 'make fuzz' exercise the package alongside the existing entries.
Add changes for the string-codec wrappers, the combined payment validator, and the fuzz harnesses in bolt12.
b359c2b to
a80db77
Compare
Based on #11061, part of #10736.
Adds
Decode/EncodeOfferString,Decode/EncodeInvoiceRequestString, andDecode/EncodeInvoiceString, composing bech32, the per-message TLV codec, and the spec reader gates into one call as the package's consumer entry point. Gates run againstBolt12Features, defined here. Wire-form encodes require and verify a populated signature, while pre-signEncodestays permitted on(*X).Encodefor Merkle-root computation.We add a
ValidateInvoiceForPaymentmethod that is a validator that should be used to validate a received invoice against a sent invoice request.Also adds fuzz harnesses for the codec decoders and wires
bolt12intomake fuzz. Byte-level targets pin no-panic decode and byte-identical round-trips, string-level targets pin no-panic through the reader gates, and two more pin the bech32 bijection and Merkle-root determinism. Seeds come from the spec test vectors.