Skip to content

Commit

Permalink
btcec/schnorr/musig2: add safer signing API with Session+Context
Browse files Browse the repository at this point in the history
In this commit, we introduce an easier to use API for musig2 signing in
the Session and Context structs.

The Context struct represents a particular musig2 signing context which
is defined by the set of signers. The struct can be serialized to disk
as it contains no volatile information. A given context can be kept for
each signer in the final set.

The Session struct represents an ephemeral musig2 signing session. It
handles nonce generation, key aggregation, nonce combination, signature
combination, and final sig verification all in one API. The API also
protects against nonce generation by not exposing nonces to the end user
and also attempting to catch nonce re-use (assuming no process forking)
across sessions.
  • Loading branch information
Roasbeef committed Mar 15, 2022
1 parent 1186a89 commit 75b9f33
Show file tree
Hide file tree
Showing 7 changed files with 405 additions and 83 deletions.
40 changes: 17 additions & 23 deletions btcec/schnorr/musig2/bench_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2013-2016 The btcsuite developers
// Copyright 2013-2022 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

Expand All @@ -11,7 +11,6 @@ import (

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)

var (
Expand Down Expand Up @@ -74,19 +73,12 @@ var (
// signature factoring in if the keys are sorted and also if we're in fast sign
// mode.
func BenchmarkPartialSign(b *testing.B) {
privKey := secp256k1.NewPrivateKey(testPrivBytes)

for _, numSigners := range []int{10, 100} {
for _, fastSign := range []bool{true, false} {
for _, sortKeys := range []bool{true, false} {
name := fmt.Sprintf("num_signers=%v/fast_sign=%v/sort=%v",
numSigners, fastSign, sortKeys)

nonces, err := GenNonces()
if err != nil {
b.Fatalf("unable to generate nonces: %v", err)
}

signers := make(signerSet, numSigners)
for i := 0; i < numSigners; i++ {
signers[i] = genSigner(b)
Expand Down Expand Up @@ -118,9 +110,12 @@ func BenchmarkPartialSign(b *testing.B) {

for i := 0; i < b.N; i++ {
sig, err = Sign(
nonces.SecNonce, privKey, combinedNonce,
keys, msg, signOpts...,
signers[0].nonces.SecNonce, signers[0].privKey,
combinedNonce, keys, msg, signOpts...,
)
if err != nil {
b.Fatalf("unable to generate sig: %v", err)
}
}

testSig = sig
Expand All @@ -138,18 +133,11 @@ var sigOk bool
// BenchmarkPartialVerify benchmarks how long it takes to verify a partial
// signature.
func BenchmarkPartialVerify(b *testing.B) {
privKey := secp256k1.NewPrivateKey(testPrivBytes)

for _, numSigners := range []int{10, 100} {
for _, sortKeys := range []bool{true, false} {
name := fmt.Sprintf("sort_keys=%v/num_signers=%v",
sortKeys, numSigners)

nonces, err := GenNonces()
if err != nil {
b.Fatalf("unable to generate nonces: %v", err)
}

signers := make(signerSet, numSigners)
for i := 0; i < numSigners; i++ {
signers[i] = genSigner(b)
Expand All @@ -172,12 +160,15 @@ func BenchmarkPartialVerify(b *testing.B) {
b.ResetTimer()

sig, err = Sign(
nonces.SecNonce, privKey, combinedNonce,
signers.keys(), msg, WithFastSign(),
signers[0].nonces.SecNonce, signers[0].privKey,
combinedNonce, signers.keys(), msg,
)
if err != nil {
b.Fatalf("unable to generate sig: %v", err)
}

keys := signers.keys()
pubKey := privKey.PubKey()
pubKey := signers[0].pubKey

b.Run(name, func(b *testing.B) {
var signOpts []SignOption
Expand All @@ -193,9 +184,12 @@ func BenchmarkPartialVerify(b *testing.B) {
var ok bool
for i := 0; i < b.N; i++ {
ok = sig.Verify(
nonces.PubNonce, combinedNonce,
signers[0].nonces.PubNonce, combinedNonce,
keys, pubKey, msg,
)
if !ok {
b.Fatalf("generated invalid sig!")
}
}
sigOk = ok
})
Expand Down Expand Up @@ -301,7 +295,7 @@ func BenchmarkAggregateKeys(b *testing.B) {
name := fmt.Sprintf("num_signers=%v/sort_keys=%v",
numSigners, sortKeys)

uniqueKeyIndex := secondUniqueKeyIndex(signerKeys)
uniqueKeyIndex := secondUniqueKeyIndex(signerKeys, false)

b.Run(name, func(b *testing.B) {
b.ResetTimer()
Expand Down
280 changes: 280 additions & 0 deletions btcec/schnorr/musig2/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
// Copyright (c) 2013-2022 The btcsuite developers

package musig2

import (
"fmt"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
)

var (
// ErrSignerNotInKeySet is returned when a the private key for a signer
// isn't included in the set of signing public keys.
ErrSignerNotInKeySet = fmt.Errorf("signing key is not found in key" +
" set")

// ErrAlredyHaveAllNonces is called when RegisterPubNonce is called too
// many times for a given signing session.
ErrAlredyHaveAllNonces = fmt.Errorf("already have all nonces")

// ErrAlredyHaveAllSigs is called when CombineSig is called too many
// times for a given signing session.
ErrAlredyHaveAllSigs = fmt.Errorf("already have all sigs")

// ErrSigningContextReuse is returned if a user attempts to sign using
// the same signing context more than once.
ErrSigningContextReuse = fmt.Errorf("nonce already used")

// ErrFinalSigInvalid is returned when the combined signature turns out
// to be invalid.
ErrFinalSigInvalid = fmt.Errorf("final signature is invalid")
)

// Context is a managed signing context for musig2. It takes care of things
// like securely generating secret nonces, aggregating keys and nonces, etc.
type Context struct {
// signingKey is the key we'll use for signing.
signingKey *btcec.PrivateKey

// pubKey is our even-y coordinate public key.
pubKey *btcec.PublicKey

// keySet is the set of all signers.
keySet []*btcec.PublicKey

// combinedKey is the aggregated public key.
combinedKey *btcec.PublicKey

// uniqueKeyIndex is the index of the second unique key in the keySet.
// This is used to speed up signing and verification computations.
uniqueKeyIndex int

// keysHash is the hash of all the keys as defined in musgi2.
keysHash []byte

// shouldSort keeps track of if the public keys should be sorted before
// any operations.
shouldSort bool
}

// NewContext creates a new signing context with the passed singing key and set
// of public keys for each of the other signers.
//
// NOTE: This struct should be used over the raw Sign API whenever possible.
func NewContext(signingKey *btcec.PrivateKey,
signers []*btcec.PublicKey, shouldSort bool) (*Context, error) {

// As a sanity check, make sure the signing key is actually amongst the sit
// of signers.
//
// TODO(roasbeef): instead have pass all the _other_ signers?
pubKey, err := schnorr.ParsePubKey(
schnorr.SerializePubKey(signingKey.PubKey()),
)
if err != nil {
return nil, err
}

var keyFound bool
for _, key := range signers {
if key.IsEqual(pubKey) {
keyFound = true
break
}
}
if !keyFound {
return nil, ErrSignerNotInKeySet
}

// Now that we know that we're actually a signer, we'll generate the
// key hash finger print and second unique key index so we can speed up
// signing later.
keysHash := keyHashFingerprint(signers, shouldSort)
uniqueKeyIndex := secondUniqueKeyIndex(signers, shouldSort)

// Next, we'll use this information to compute the aggregated public
// key that'll be used for signing in practice.
combinedKey := AggregateKeys(
signers, shouldSort, WithKeysHash(keysHash),
WithUniqueKeyIndex(uniqueKeyIndex),
)

return &Context{
signingKey: signingKey,
pubKey: pubKey,
keySet: signers,
combinedKey: combinedKey,
uniqueKeyIndex: uniqueKeyIndex,
keysHash: keysHash,
shouldSort: shouldSort,
}, nil
}

// CombinedKey returns the combined public key that will be used to generate
// multi-signatures against.
func (c *Context) CombinedKey() btcec.PublicKey {
return *c.combinedKey
}

// PubKey returns the public key of the signer of this session.
func (c *Context) PubKey() btcec.PublicKey {
return *c.pubKey
}

// Session represents a musig2 signing session. A new instance should be
// created each time a multi-signature is needed. The session struct handles
// nonces management, incremental partial sig vitrifaction, as well as final
// signature combination. Errors are returned when unsafe behavior such as
// nonce re-use is attempted.
//
// NOTE: This struct should be used over the raw Sign API whenever possible.
type Session struct {
ctx *Context

localNonces *Nonces

pubNonces [][PubNonceSize]byte

combinedNonce [PubNonceSize]byte

msg [32]byte

ourSig *PartialSignature
sigs []*PartialSignature

finalSig *schnorr.Signature
}

// NewSession creates a new musig2 signing session.
func (c *Context) NewSession() (*Session, error) {
localNonces, err := GenNonces()
if err != nil {
return nil, err
}

s := &Session{
ctx: c,
localNonces: localNonces,
pubNonces: make([][PubNonceSize]byte, 0, len(c.keySet)),
sigs: make([]*PartialSignature, 0, len(c.keySet)),
}

s.pubNonces = append(s.pubNonces, localNonces.PubNonce)

return s, nil
}

// PublicNonce returns the public nonce for a signer. This should be sent to
// other parties before signing begins, so they can compute the aggregated
// public nonce.
func (s *Session) PublicNonce() [PubNonceSize]byte {
return s.localNonces.PubNonce
}

// RegisterPubNonce should be called for each public nonce from the set of
// signers. This method returns true once all the public nonces have been
// accounted for.
func (s *Session) RegisterPubNonce(nonce [PubNonceSize]byte) (bool, error) {
// If we already have all the nonces, then this method was called too many
// times.
haveAllNonces := len(s.pubNonces) == len(s.ctx.keySet)
if haveAllNonces {
return false, nil
}

// Add this nonce and check again if we already have tall the nonces we
// need.
s.pubNonces = append(s.pubNonces, nonce)
haveAllNonces = len(s.pubNonces) == len(s.ctx.keySet)

// If we have all the nonces, then we can go ahead and combine them
// now.
if haveAllNonces {
combinedNonce, err := AggregateNonces(s.pubNonces)
if err != nil {
return false, err
}

s.combinedNonce = combinedNonce
}

return haveAllNonces, nil
}

// Sign generates a partial signature for the target message, using the target
// context. If this method is called more than once per context, then an error
// is returned, as that means a nonce was re-used.
func (s *Session) Sign(msg [32]byte,
signOpts ...SignOption) (*PartialSignature, error) {

s.msg = msg

// If no local nonce is present, then this means we already signed, so
// we'll return an error to prevent nonce re-use.
if s.localNonces == nil {
return nil, ErrSigningContextReuse
}

partialSig, err := Sign(
s.localNonces.SecNonce, s.ctx.signingKey, s.combinedNonce,
s.ctx.keySet, msg, signOpts...,
)

// Now that we've generated our signature, we'll make sure to blank out
// our signing nonce.
s.localNonces = nil

if err != nil {
return nil, err
}

s.ourSig = partialSig
s.sigs = append(s.sigs, partialSig)

return partialSig, nil
}

// CombineSigs buffers a partial signature received from a signing party. The
// method returns true once all the signatures are available, and can be
// combined into the final signature.
func (s *Session) CombineSig(sig *PartialSignature) (bool, error) {
// First check if we already have all the signatures we need. We
// already accumulated our own signature when we generated the sig.
haveAllSigs := len(s.sigs) == len(s.ctx.keySet)
if haveAllSigs {
return false, ErrAlredyHaveAllSigs
}

// TODO(roasbeef): incremental check for invalid sig, or just detect at
// the very end?

// Accumulate this sig, and check again if we have all the sigs we
// need.
s.sigs = append(s.sigs, sig)
haveAllSigs = len(s.sigs) == len(s.ctx.keySet)

// If we have all the signatures, then we can combine them all into the
// final signature.
if haveAllSigs {
finalSig := CombineSigs(s.ourSig.R, s.sigs)

// We'll also verify the signature at this point to ensure it's
// valid.
//
// TODO(roasbef): allow skipping?
if !finalSig.Verify(s.msg[:], s.ctx.combinedKey) {
return false, ErrFinalSigInvalid
}

s.finalSig = finalSig
}

return haveAllSigs, nil
}

// FinalSig returns the final combined multi-signature, if present.
func (s *Session) FinalSig() *schnorr.Signature {
return s.finalSig
}
Loading

0 comments on commit 75b9f33

Please sign in to comment.