From 7517295ee0b1ec5d9be7a2bb0bcb3c457a7ec74f Mon Sep 17 00:00:00 2001 From: Erick Cestari Date: Thu, 19 Mar 2026 11:11:03 -0300 Subject: [PATCH] lnwire: validate MuSig2 nonce points on wire decode Add point-on-curve validation for MuSig2 public nonces at the TLV decode layer. A MuSig2 nonce is 66 bytes (two 33-byte compressed secp256k1 public keys). Previously, nonce bytes were accepted without validation, with invalid points only failing later during MuSig2 session creation deep in the signing flow. Now, malformed nonces from a peer are rejected immediately at decode time with clear errors. This hardens all nonce-carrying messages: ClosingComplete (JIT closer nonces in PartialSigWithNonce), ClosingSig (NextCloseeNonce), Shutdown, ChannelReestablish, CommitSig, and others. --- lnwire/commit_sig_test.go | 10 +++++++--- lnwire/musig2.go | 27 ++++++++++++++++++++++++++- lnwire/musig2_test.go | 23 ++++++++++++++++++----- lnwire/partial_sig.go | 4 ++++ lnwire/test_message.go | 6 +----- lnwire/test_utils.go | 11 ++++++++--- 6 files changed, 64 insertions(+), 17 deletions(-) diff --git a/lnwire/commit_sig_test.go b/lnwire/commit_sig_test.go index 0772a2fb8..2524727d8 100644 --- a/lnwire/commit_sig_test.go +++ b/lnwire/commit_sig_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/lightningnetwork/lnd/tlv" "github.com/stretchr/testify/require" ) @@ -42,8 +41,13 @@ func generateCommitSigTestCases(t *testing.T) []commitSigTestCase { sigScalar := new(btcec.ModNScalar) sigScalar.SetByteSlice(sig.RawBytes()) - var nonce [musig2.PubNonceSize]byte - copy(nonce[:], commitSigBytes) + // Generate a valid MuSig2 nonce (two compressed public keys). + _, pub1 := btcec.PrivKeyFromBytes(chanIDBytes) + _, pub2 := btcec.PrivKeyFromBytes(commitSigBytes[:32]) + + var nonce Musig2Nonce + copy(nonce[:33], pub1.SerializeCompressed()) + copy(nonce[33:], pub2.SerializeCompressed()) sigWithNonce := NewPartialSigWithNonce(nonce, *sigScalar) partialSig := MaybePartialSigWithNonce(sigWithNonce) diff --git a/lnwire/musig2.go b/lnwire/musig2.go index 10dbc27af..4b69b7063 100644 --- a/lnwire/musig2.go +++ b/lnwire/musig2.go @@ -1,8 +1,10 @@ package lnwire import ( + "fmt" "io" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/lightningnetwork/lnd/tlv" ) @@ -47,13 +49,36 @@ func nonceTypeEncoder(w io.Writer, val interface{}, _ *[8]byte) error { return tlv.NewTypeForEncodingErr(val, "lnwire.Musig2Nonce") } +// ValidateMusig2Nonce checks that a 66-byte MuSig2 public nonce contains two +// valid compressed secp256k1 points. +func ValidateMusig2Nonce(nonce Musig2Nonce) error { + const compressedKeyLen = 33 + + // A MuSig2 public nonce is two 33-byte compressed public keys (R1, R2). + _, err := btcec.ParsePubKey(nonce[:compressedKeyLen]) + if err != nil { + return fmt.Errorf("invalid first nonce point: %w", err) + } + + _, err = btcec.ParsePubKey(nonce[compressedKeyLen:]) + if err != nil { + return fmt.Errorf("invalid second nonce point: %w", err) + } + + return nil +} + // nonceTypeDecoder is a custom TLV decoder for the Musig2Nonce record. func nonceTypeDecoder(r io.Reader, val interface{}, _ *[8]byte, l uint64) error { if v, ok := val.(*Musig2Nonce); ok && l == musig2.PubNonceSize { _, err := io.ReadFull(r, v[:]) - return err + if err != nil { + return err + } + + return ValidateMusig2Nonce(*v) } return tlv.NewTypeForDecodingErr( diff --git a/lnwire/musig2_test.go b/lnwire/musig2_test.go index eac4a7286..8f55b5a80 100644 --- a/lnwire/musig2_test.go +++ b/lnwire/musig2_test.go @@ -3,17 +3,30 @@ package lnwire import ( "testing" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/stretchr/testify/require" ) -// makeNonce creates a test Musig2Nonce with sequential byte values for testing -// TLV encoding/decoding. +// makeNonce creates a test Musig2Nonce containing two valid compressed public +// keys for testing TLV encoding/decoding. func makeNonce() Musig2Nonce { + _, pub1 := btcec.PrivKeyFromBytes([]byte{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + }) + _, pub2 := btcec.PrivKeyFromBytes([]byte{ + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, + 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, + 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, + }) + var n Musig2Nonce - for i := range n { - n[i] = byte(i) - } + copy(n[:33], pub1.SerializeCompressed()) + copy(n[33:], pub2.SerializeCompressed()) return n } diff --git a/lnwire/partial_sig.go b/lnwire/partial_sig.go index 1751ae5ce..d5af8ec90 100644 --- a/lnwire/partial_sig.go +++ b/lnwire/partial_sig.go @@ -210,6 +210,10 @@ func partialSigWithNonceTypeDecoder(r io.Reader, val interface{}, buf *[8]byte, return err } + if err := ValidateMusig2Nonce(nonce); err != nil { + return err + } + *v = PartialSigWithNonce{ PartialSig: NewPartialSig(s), Nonce: nonce, diff --git a/lnwire/test_message.go b/lnwire/test_message.go index 9aae7d273..9af7621d3 100644 --- a/lnwire/test_message.go +++ b/lnwire/test_message.go @@ -1742,11 +1742,7 @@ func (c *RevokeAndAck) RandTestMessage(t *rapid.T) Message { msg.NextRevocationKey = RandPubKey(t) if rapid.Bool().Draw(t, "includeLocalNonce") { - var nonce Musig2Nonce - nonceBytes := rapid.SliceOfN(rapid.Byte(), 32, 32).Draw( - t, "nonce", - ) - copy(nonce[:], nonceBytes) + nonce := RandMusig2Nonce(t) msg.LocalNonce = tlv.SomeRecordT( tlv.NewRecordT[NonceRecordTypeT, Musig2Nonce](nonce), diff --git a/lnwire/test_utils.go b/lnwire/test_utils.go index 227640a43..602724a9d 100644 --- a/lnwire/test_utils.go +++ b/lnwire/test_utils.go @@ -299,11 +299,16 @@ func RandTLVRecords(t *rapid.T, ignoreRecords fn.Set[uint64], return customRecords, ignoreSet } -// RandMusig2Nonce generates a random musig2 nonce. +// RandMusig2Nonce generates a random musig2 nonce containing two valid +// compressed secp256k1 public keys. func RandMusig2Nonce(t *rapid.T) Musig2Nonce { + // A MuSig2 public nonce is two 33-byte compressed public keys. + pub1 := RandPubKey(t) + pub2 := RandPubKey(t) + var nonce Musig2Nonce - bytes := rapid.SliceOfN(rapid.Byte(), 32, 32).Draw(t, "nonce") - copy(nonce[:], bytes) + copy(nonce[:33], pub1.SerializeCompressed()) + copy(nonce[33:], pub2.SerializeCompressed()) return nonce }