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.
This commit is contained in:
Erick Cestari 2026-03-19 11:11:03 -03:00
parent 21ee770e8c
commit 7517295ee0
No known key found for this signature in database
GPG key ID: D7D17E26F2FC3F3C
6 changed files with 64 additions and 17 deletions

View file

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

View file

@ -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(

View file

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

View file

@ -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,

View file

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

View file

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