Files
zeekayandHanzo Dev 3685ebea4c mpc: make signature verification real, and stop trusting a caller's Y
Both protocol adapters shipped a Verify that returned false unconditionally —
cggmp21 with "For now, return false", frost with "doesn't support ECDSA". That
is not a safe default dressed up as one. A verifier that always fails is
indistinguishable from a verifier that works and was handed a bad signature, so
a caller using it as a signing-path health check sees a permanent, unexplained
failure, and a caller treating "unsupported" as skippable gets no verification
at all.

Both now verify. FROST is Schnorr rather than ECDSA, but its group key is still
a secp256k1 point and *ecdsa.PublicKey is only how this package's interface
carries one, so the point is recovered and handed to the FROST verifier.

pkg/protocol/curvekey.go holds the conversion both adapters need, replacing two
hand-rolled decompressions (a Tonelli-Shanks square root in cggmp21, a separate
helper in frost). CurvePoint goes the other way, and it validates rather than
assumes: an (X, Y) that is not on the curve is REJECTED, not silently corrected.
Compression keeps only the parity of Y, so a conversion that trusts X and
parity alone will happily turn an inconsistent key into a valid point and then
verify signatures against a key the caller never named. X=1 is a real X
coordinate on secp256k1, which is how the test that found this was written.

X is left-padded with FillBytes; big.Int drops leading zeros, so about one key
in 256 would otherwise encode short and fail to convert at all.

Bumps luxfi/threshold to v1.12.5 for pkg/quorum.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-25 11:45:23 -07:00

86 lines
2.5 KiB
Go

package cggmp21
import (
"crypto/rand"
"testing"
tecdsa "github.com/luxfi/threshold/pkg/ecdsa"
"github.com/luxfi/threshold/pkg/math/curve"
"github.com/luxfi/threshold/pkg/math/sample"
"github.com/luxfi/mpc/pkg/protocol"
)
// newTestSignature produces a genuine ECDSA signature over hash under secret
// x, using the same curve arithmetic the threshold protocol outputs.
func newTestSignature(t *testing.T, x curve.Scalar, hash []byte) *tecdsa.Signature {
t.Helper()
group := x.Curve()
k := sample.Scalar(rand.Reader, group)
m := curve.FromHash(group, hash)
kInv := group.NewScalar().Set(k).Invert()
R := kInv.ActOnBase()
r := R.XScalar()
s := r.Mul(x).Add(m).Mul(k)
return &tecdsa.Signature{R: R, S: s}
}
// signatureAdapter.Verify used to `return false` unconditionally with the
// comment "For now". Any caller using it as a signing-path health check saw a
// permanent, inexplicable failure; any caller treating an unverifiable
// signature as acceptable got no verification at all. This pins that it now
// actually verifies.
func TestSignatureAdapterVerifiesRealSignature(t *testing.T) {
group := curve.Secp256k1{}
for i := 0; i < 64; i++ {
x := sample.Scalar(rand.Reader, group)
pub := protocol.PublicKey(x.ActOnBase())
if pub == nil {
t.Fatalf("iteration %d: could not render public key", i)
}
hash := make([]byte, 32)
if _, err := rand.Read(hash); err != nil {
t.Fatalf("rand: %v", err)
}
adapter := &signatureAdapter{sig: newTestSignature(t, x, hash)}
if !adapter.Verify(pub, hash) {
t.Fatalf("iteration %d: Verify rejected a valid signature", i)
}
}
}
func TestSignatureAdapterRejectsBadInput(t *testing.T) {
group := curve.Secp256k1{}
x := sample.Scalar(rand.Reader, group)
pub := protocol.PublicKey(x.ActOnBase())
hash := make([]byte, 32)
copy(hash, "the message that was signed-----")
adapter := &signatureAdapter{sig: newTestSignature(t, x, hash)}
otherHash := make([]byte, 32)
copy(otherHash, "a different message entirely----")
otherPub := protocol.PublicKey(sample.Scalar(rand.Reader, group).ActOnBase())
for _, tc := range []struct {
name string
run func() bool
}{
{"wrong message", func() bool { return adapter.Verify(pub, otherHash) }},
{"wrong public key", func() bool { return adapter.Verify(otherPub, hash) }},
{"nil public key", func() bool { return adapter.Verify(nil, hash) }},
{"nil signature", func() bool { return (&signatureAdapter{}).Verify(pub, hash) }},
} {
t.Run(tc.name, func(t *testing.T) {
if tc.run() {
t.Fatal("Verify accepted something it must reject")
}
})
}
}