From 03f6c5cd0a8141f40de689eeeae5cb2d30e1c6b5 Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Wed, 14 Dec 2022 15:02:01 -0500 Subject: [PATCH] htlcswitch: add blinding kit to handle encrypted data in blinded routes This commit introduces a blinding kits which abstracts over the operations required to decrypt, deserialize and reconstruct forwarding data from an encrypted blob of data included for nodes in blinded routes. --- htlcswitch/hop/iterator.go | 136 +++++++++++++++++++++++++++++++ htlcswitch/hop/iterator_test.go | 139 ++++++++++++++++++++++++++++++++ 2 files changed, 275 insertions(+) diff --git a/htlcswitch/hop/iterator.go b/htlcswitch/hop/iterator.go index 095fd190d..7ddb9af77 100644 --- a/htlcswitch/hop/iterator.go +++ b/htlcswitch/hop/iterator.go @@ -2,6 +2,7 @@ package hop import ( "bytes" + "errors" "fmt" "io" "sync" @@ -9,9 +10,15 @@ import ( "github.com/btcsuite/btcd/btcec/v2" sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/tlv" ) +var ( + // ErrDecodeFailed is returned when we can't decode blinded data. + ErrDecodeFailed = errors.New("could not decode blinded data") +) + // Iterator is an interface that abstracts away the routing information // included in HTLC's which includes the entirety of the payment path of an // HTLC. This interface provides two basic method which carry out: how to @@ -114,6 +121,135 @@ func (r *sphinxHopIterator) ExtractErrorEncrypter( return extracter(r.ogPacket.EphemeralKey) } +// BlindingProcessor is an interface that provides the cryptographic operations +// required for processing blinded hops. +// +// This interface is extracted to allow more granular testing of blinded +// forwarding calculations. +type BlindingProcessor interface { + // DecryptBlindedHopData decrypts a blinded blob of data using the + // ephemeral key provided. + DecryptBlindedHopData(ephemPub *btcec.PublicKey, + encryptedData []byte) ([]byte, error) +} + +// BlindingKit contains the components required to extract forwarding +// information for hops in a blinded route. +type BlindingKit struct { + // Processor provides the low-level cryptographic operations to + // handle an encrypted blob of data in a blinded forward. + Processor BlindingProcessor + + // UpdateAddBlinding holds a blinding point that was passed to the + // node via update_add_htlc's TLVs. + UpdateAddBlinding lnwire.BlindingPointRecord + + // IncomingCltv is the expiry of the incoming HTLC. + IncomingCltv uint32 + + // IncomingAmount is the amount of the incoming HTLC. + IncomingAmount lnwire.MilliSatoshi +} + +// validateBlindingPoint validates that only one blinding point is present for +// the hop and returns the relevant one. +func (b *BlindingKit) validateBlindingPoint(payloadBlinding *btcec.PublicKey, + isFinalHop bool) (*btcec.PublicKey, error) { + + // Bolt 04: if encrypted_recipient_data is present: + // - if blinding_point (in update add) is set: + // - MUST error if current_blinding_point is set (in payload) + // - otherwise: + // - MUST return an error if current_blinding_point is not present + // (in payload) + payloadBlindingSet := payloadBlinding != nil + updateBlindingSet := b.UpdateAddBlinding.IsSome() + + switch { + case !(payloadBlindingSet || updateBlindingSet): + return nil, ErrInvalidPayload{ + Type: record.BlindingPointOnionType, + Violation: OmittedViolation, + FinalHop: isFinalHop, + } + + case payloadBlindingSet && updateBlindingSet: + return nil, ErrInvalidPayload{ + Type: record.BlindingPointOnionType, + Violation: IncludedViolation, + FinalHop: isFinalHop, + } + + case payloadBlindingSet: + return payloadBlinding, nil + + case updateBlindingSet: + pk, err := b.UpdateAddBlinding.UnwrapOrErr( + fmt.Errorf("expected update add blinding"), + ) + if err != nil { + return nil, err + } + + return pk.Val, nil + } + + return nil, fmt.Errorf("expected blinded point set") +} + +// DecryptAndValidateFwdInfo performs all operations required to decrypt and +// validate a blinded route. +func (b *BlindingKit) DecryptAndValidateFwdInfo(payload *Payload, + isFinalHop bool) (*ForwardingInfo, error) { + + // We expect this function to be called when we have encrypted data + // present, and a blinding key is set either in the payload or the + // update_add_htlc message. + blindingPoint, err := b.validateBlindingPoint( + payload.blindingPoint, isFinalHop, + ) + if err != nil { + return nil, err + } + + decrypted, err := b.Processor.DecryptBlindedHopData( + blindingPoint, payload.encryptedData, + ) + if err != nil { + return nil, fmt.Errorf("decrypt blinded "+ + "data: %w", err) + } + + buf := bytes.NewBuffer(decrypted) + routeData, err := record.DecodeBlindedRouteData(buf) + if err != nil { + return nil, fmt.Errorf("%w: %w", + ErrDecodeFailed, err) + } + + if err := ValidateBlindedRouteData( + routeData, b.IncomingAmount, b.IncomingCltv, + ); err != nil { + return nil, err + } + + fwdAmt, err := calculateForwardingAmount( + b.IncomingAmount, routeData.RelayInfo.Val.BaseFee, + routeData.RelayInfo.Val.FeeRate, + ) + if err != nil { + return nil, err + } + + return &ForwardingInfo{ + NextHop: routeData.ShortChannelID.Val, + AmountToForward: fwdAmt, + OutgoingCTLV: b.IncomingCltv - uint32( + routeData.RelayInfo.Val.CltvExpiryDelta, + ), + }, nil +} + // calculateForwardingAmount calculates the amount to forward for a blinded // hop based on the incoming amount and forwarding parameters. // diff --git a/htlcswitch/hop/iterator_test.go b/htlcswitch/hop/iterator_test.go index 74eb60a7a..b8b8b4f72 100644 --- a/htlcswitch/hop/iterator_test.go +++ b/htlcswitch/hop/iterator_test.go @@ -3,8 +3,10 @@ package hop import ( "bytes" "encoding/binary" + "errors" "testing" + "github.com/btcsuite/btcd/btcec/v2" "github.com/davecgh/go-spew/spew" sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/lnwire" @@ -153,3 +155,140 @@ func TestForwardingAmountCalc(t *testing.T) { }) } } + +// mockProcessor is a mocked blinding point processor that just returns the +// data that it is called with when "decrypting". +type mockProcessor struct { + decryptErr error +} + +// DecryptBlindedHopData mocks blob decryption, returning the same data that +// it was called with and an optionally configured error. +func (m *mockProcessor) DecryptBlindedHopData(_ *btcec.PublicKey, + data []byte) ([]byte, error) { + + return data, m.decryptErr +} + +// TestDecryptAndValidateFwdInfo tests deriving forwarding info using a +// blinding kit. This test does not cover assertions on the calculations of +// forwarding information, because this is covered in a test dedicated to those +// calculations. +func TestDecryptAndValidateFwdInfo(t *testing.T) { + t.Parallel() + + // Encode valid blinding data that we'll fake decrypting for our test. + maxCltv := 1000 + blindedData := record.NewBlindedRouteData( + lnwire.NewShortChanIDFromInt(1500), nil, + record.PaymentRelayInfo{ + CltvExpiryDelta: 10, + BaseFee: 100, + FeeRate: 0, + }, + &record.PaymentConstraints{ + MaxCltvExpiry: 1000, + HtlcMinimumMsat: lnwire.MilliSatoshi(1), + }, + nil, + ) + + validData, err := record.EncodeBlindedRouteData(blindedData) + require.NoError(t, err) + + // Mocked error. + errDecryptFailed := errors.New("could not decrypt") + + tests := []struct { + name string + data []byte + incomingCLTV uint32 + updateAddBlinding *btcec.PublicKey + payloadBlinding *btcec.PublicKey + processor *mockProcessor + expectedErr error + }{ + { + name: "no blinding point", + data: validData, + processor: &mockProcessor{}, + expectedErr: ErrInvalidPayload{ + Type: record.BlindingPointOnionType, + Violation: OmittedViolation, + }, + }, + { + name: "both blinding points", + data: validData, + updateAddBlinding: &btcec.PublicKey{}, + payloadBlinding: &btcec.PublicKey{}, + processor: &mockProcessor{}, + expectedErr: ErrInvalidPayload{ + Type: record.BlindingPointOnionType, + Violation: IncludedViolation, + }, + }, + { + name: "decryption failed", + data: validData, + updateAddBlinding: &btcec.PublicKey{}, + incomingCLTV: 500, + processor: &mockProcessor{ + decryptErr: errDecryptFailed, + }, + expectedErr: errDecryptFailed, + }, + { + name: "decode fails", + data: []byte{1, 2, 3}, + updateAddBlinding: &btcec.PublicKey{}, + incomingCLTV: 500, + processor: &mockProcessor{}, + expectedErr: ErrDecodeFailed, + }, + { + name: "validation fails", + data: validData, + updateAddBlinding: &btcec.PublicKey{}, + incomingCLTV: uint32(maxCltv) + 10, + processor: &mockProcessor{}, + expectedErr: ErrInvalidPayload{ + Type: record.LockTimeOnionType, + Violation: InsufficientViolation, + }, + }, + { + name: "valid", + updateAddBlinding: &btcec.PublicKey{}, + data: validData, + processor: &mockProcessor{}, + expectedErr: nil, + }, + } + + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + // We don't actually use blinding keys due to our + // mocking so they can be nil. + kit := BlindingKit{ + Processor: testCase.processor, + IncomingAmount: 10000, + IncomingCltv: testCase.incomingCLTV, + } + + if testCase.updateAddBlinding != nil { + kit.UpdateAddBlinding = tlv.SomeRecordT( + //nolint:lll + tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](testCase.updateAddBlinding), + ) + } + _, err := kit.DecryptAndValidateFwdInfo( + &Payload{ + encryptedData: testCase.data, + blindingPoint: testCase.payloadBlinding, + }, false, + ) + require.ErrorIs(t, err, testCase.expectedErr) + }) + } +}