Skip to content

Commit

Permalink
Merge pull request #7669 from morehouse/fuzz_lnwire_onion_errors
Browse files Browse the repository at this point in the history
lnwire: fuzz onion failure messages
  • Loading branch information
Roasbeef authored Oct 30, 2023
2 parents 78ff68a + 66d6a84 commit 32c8b82
Show file tree
Hide file tree
Showing 2 changed files with 204 additions and 0 deletions.
5 changes: 5 additions & 0 deletions docs/release-notes/release-notes-0.18.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@
# Technical and Architectural Updates
## BOLT Spec Updates
## Testing

* Added fuzz tests for [onion
errors](https://github.com/lightningnetwork/lnd/pull/7669).

## Database

* [Add context to InvoiceDB
Expand All @@ -102,5 +106,6 @@
* Carla Kirk-Cohen
* Elle Mouton
* Keagan McClelland
* Matt Morehouse
* Ononiwu Maureen Chiamaka
* Yong Yu
199 changes: 199 additions & 0 deletions lnwire/fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"compress/zlib"
"encoding/binary"
"reflect"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -635,3 +636,201 @@ func FuzzConvertFixedSignature(f *testing.F) {
require.Equal(t, derBytes, derBytes2, "signature mismatch")
})
}

// prefixWithFailCode adds a failure code prefix to data.
func prefixWithFailCode(data []byte, code FailCode) []byte {
var codeBytes [2]byte
binary.BigEndian.PutUint16(codeBytes[:], uint16(code))
data = append(codeBytes[:], data...)

return data
}

// equalFunc is a function used to determine whether two deserialized messages
// are equivalent.
type equalFunc func(x, y any) bool

// onionFailureHarnessCustom performs the actual fuzz testing of the appropriate
// onion failure message. This function will check that the passed-in message
// passes wire length checks, is a valid message once deserialized, and passes a
// sequence of serialization and deserialization checks.
func onionFailureHarnessCustom(t *testing.T, data []byte, code FailCode,
eq equalFunc) {

data = prefixWithFailCode(data, code)

// Don't waste time fuzzing messages larger than we'll ever accept.
if len(data) > MaxSliceLength {
return
}

// First check whether the failure message can be decoded.
r := bytes.NewReader(data)
msg, err := DecodeFailureMessage(r, 0)
if err != nil {
return
}

// We now have a valid decoded message. Verify that encoding and
// decoding the message does not mutate it.

var b bytes.Buffer
err = EncodeFailureMessage(&b, msg, 0)
require.NoError(t, err, "failed to encode failure message")

newMsg, err := DecodeFailureMessage(&b, 0)
require.NoError(t, err, "failed to decode serialized failure message")

require.True(
t, eq(msg, newMsg),
"original message and deserialized message are not equal: "+
"%v != %v",
msg, newMsg,
)

// Now verify that encoding/decoding full packets works as expected.

var pktBuf bytes.Buffer
if err := EncodeFailure(&pktBuf, msg, 0); err != nil {
// EncodeFailure returns an error if the encoded message would
// exceed FailureMessageLength bytes, as LND always encodes
// fixed-size packets for privacy. But it is valid to decode
// messages longer than this, so we should not report an error
// if the original message was longer.
//
// We add 2 to the length of the original message since it may
// have omitted a channel_update type prefix of 2 bytes. When
// we re-encode such a message, we will add the 2-byte prefix
// as prescribed by the spec.
if len(data)+2 > FailureMessageLength {
return
}

t.Fatalf("failed to encode failure packet: %v", err)
}

// We should use FailureMessageLength sized packets plus 2 bytes to
// encode the message length and 2 bytes to encode the padding length,
// as recommended by the spec.
require.Equal(
t, pktBuf.Len(), FailureMessageLength+4,
"wrong failure message length",
)

pktMsg, err := DecodeFailure(&pktBuf, 0)
require.NoError(t, err, "failed to decode failure packet")

require.True(
t, eq(msg, pktMsg),
"original message and decoded packet message are not equal: "+
"%v != %v",
msg, pktMsg,
)
}

func onionFailureHarness(t *testing.T, data []byte, code FailCode) {
t.Helper()
onionFailureHarnessCustom(t, data, code, reflect.DeepEqual)
}

func FuzzFailIncorrectDetails(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
// Since FailIncorrectDetails.Decode can leave extraOpaqueData
// as nil while FailIncorrectDetails.Encode writes an empty
// slice, we need to use a custom equality function.
eq := func(x, y any) bool {
msg1, ok := x.(*FailIncorrectDetails)
require.True(
t, ok, "msg1 was not FailIncorrectDetails",
)

msg2, ok := y.(*FailIncorrectDetails)
require.True(
t, ok, "msg2 was not FailIncorrectDetails",
)

return msg1.amount == msg2.amount &&
msg1.height == msg2.height &&
bytes.Equal(
msg1.extraOpaqueData,
msg2.extraOpaqueData,
)
}

onionFailureHarnessCustom(
t, data, CodeIncorrectOrUnknownPaymentDetails, eq,
)
})
}

func FuzzFailInvalidOnionVersion(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
onionFailureHarness(t, data, CodeInvalidOnionVersion)
})
}

func FuzzFailInvalidOnionHmac(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
onionFailureHarness(t, data, CodeInvalidOnionHmac)
})
}

func FuzzFailInvalidOnionKey(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
onionFailureHarness(t, data, CodeInvalidOnionKey)
})
}

func FuzzFailTemporaryChannelFailure(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
onionFailureHarness(t, data, CodeTemporaryChannelFailure)
})
}

func FuzzFailAmountBelowMinimum(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
onionFailureHarness(t, data, CodeAmountBelowMinimum)
})
}

func FuzzFailFeeInsufficient(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
onionFailureHarness(t, data, CodeFeeInsufficient)
})
}

func FuzzFailIncorrectCltvExpiry(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
onionFailureHarness(t, data, CodeIncorrectCltvExpiry)
})
}

func FuzzFailExpiryTooSoon(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
onionFailureHarness(t, data, CodeExpiryTooSoon)
})
}

func FuzzFailChannelDisabled(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
onionFailureHarness(t, data, CodeChannelDisabled)
})
}

func FuzzFailFinalIncorrectCltvExpiry(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
onionFailureHarness(t, data, CodeFinalIncorrectCltvExpiry)
})
}

func FuzzFailFinalIncorrectHtlcAmount(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
onionFailureHarness(t, data, CodeFinalIncorrectHtlcAmount)
})
}

func FuzzInvalidOnionPayload(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
onionFailureHarness(t, data, CodeInvalidOnionPayload)
})
}

0 comments on commit 32c8b82

Please sign in to comment.