Files
zeekay d1cc69af98 Make the Schnorr challenge cover the message it signs
The signer and the verifier each built c = H(R || Y || m) by passing the
concatenation to curve.FromHash, which does not hash: it truncates its
input to the group order's byte length and reads it big-endian, the SECG
convention for an ECDSA digest. Y and m fell off the end, so the
challenge was a function of R alone and one signature verified under
every message.

The derivation now lives in one place and hashes, each piece length- and
domain-separated, with the digest read wide.
2026-09-01 19:31:18 -07:00

124 lines
3.2 KiB
Go

package sign
import (
"errors"
"github.com/luxfi/threshold/internal/round"
"github.com/luxfi/threshold/pkg/math/curve"
"github.com/luxfi/threshold/pkg/party"
)
// round3 combines partial signatures and verifies the final Schnorr signature
type round3 struct {
*round2
// Collected partial signatures
partialSigs map[party.ID]curve.Scalar
// Schnorr challenge c = H(R || Y || m)
challenge curve.Scalar
}
// SchnorrSignature represents a threshold Schnorr signature
type SchnorrSignature struct {
R curve.Point // Combined nonce point
Z curve.Scalar // Combined signature scalar
}
// Verify verifies a Schnorr signature: z*G == R + c*Y
func (sig *SchnorrSignature) Verify(publicKey curve.Point, messageHash []byte) bool {
if sig.R == nil || sig.Z == nil || publicKey == nil {
return false
}
group := sig.R.Curve()
// Recompute challenge: c = H(R || Y || m)
c, err := challenge(group, sig.R, publicKey, messageHash)
if err != nil {
return false
}
// Verify: z*G == R + c*Y
// Left side: z*G
left := sig.Z.ActOnBase()
// Right side: R + c*Y
cY := c.Act(publicKey)
right := sig.R.Add(cY)
return left.Equal(right)
}
// Number implements round.Round
func (r *round3) Number() round.Number {
return 3
}
// BroadcastContent implements round.BroadcastRound
func (r *round3) BroadcastContent() round.BroadcastContent {
return nil // No broadcast in round 3
}
// MessageContent implements round.Round
func (r *round3) MessageContent() round.Content {
return nil // No messages in round 3
}
// VerifyMessage implements round.Round
func (r *round3) VerifyMessage(_ round.Message) error {
return nil // No messages to verify
}
// StoreMessage implements round.Round
func (r *round3) StoreMessage(_ round.Message) error {
return nil // No messages to store
}
// Finalize implements round.Round
func (r *round3) Finalize(_ chan<- *round.Message) (round.Session, error) {
// Collect partial sigs fresh from round2's sync.Map
// This is necessary because round3 may have been created before all broadcasts
// were received (during initializeRound), so the partialSigs map may be stale
r.round2.receivedPartialSigs.Range(func(key, value interface{}) bool {
id := key.(party.ID)
r.partialSigs[id] = value.(curve.Scalar)
return true
})
// Verify we have partial signatures from all signers
// Our own partial sig is already stored in round2.receivedPartialSigs
if len(r.partialSigs) != len(r.signers) {
return nil, errors.New("missing partial signatures from some signers")
}
// Combine partial signatures: z = sum(z_i)
z := r.Group().NewScalar()
for _, partialSig := range r.partialSigs {
z = z.Add(partialSig)
}
// Create final Schnorr signature
sig := &SchnorrSignature{
R: r.R,
Z: z,
}
// Verify the signature against the public key
publicKey, err := r.config.PublicPoint()
if err != nil {
return nil, err
}
if !sig.Verify(publicKey, r.messageHash) {
return nil, errors.New("signature verification failed")
}
return r.ResultRound(sig), nil
}
// StoreBroadcastMessage implements round.BroadcastRound - no-op for round3
func (r *round3) StoreBroadcastMessage(_ round.Message) error {
return nil // Round3 doesn't receive broadcasts - partial sigs passed from round2
}