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.
This commit is contained in:
Carla Kirk-Cohen 2022-12-14 15:02:01 -05:00
parent 040fcb0f92
commit 03f6c5cd0a
No known key found for this signature in database
GPG key ID: 4CA7FE54A6213C91
2 changed files with 275 additions and 0 deletions

View file

@ -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.
//

View file

@ -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)
})
}
}