Skip to content

Commit

Permalink
New tss package, and includes tss/frost threshold signature scheme
Browse files Browse the repository at this point in the history
Changes:
 - The package tss will provide the threshold signature schemes.
 - The package tss/frost implements the FROST threshold signature
   scheme for Schnorr signatures.
   This scheme is under standardization process at IETF/CFRG [2].
   Test vectors from [3] are passing for P256 and Ristretto255 groups.
 - Version supported: v11

References:
 [1] frost paper: https://eprint.iacr.org/2020/852
 [2] draft-irtf-cfrg-frost: https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost
 [3] test-vectors: https://github.com/cfrg/draft-irtf-cfrg-frost
  • Loading branch information
armfazh committed Nov 24, 2022
1 parent dd28f0b commit a3638fe
Show file tree
Hide file tree
Showing 10 changed files with 1,076 additions and 0 deletions.
79 changes: 79 additions & 0 deletions tss/frost/combiner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package frost

import (
"errors"
"fmt"
)

type Combiner struct {
Suite
threshold uint
maxSigners uint
}

func NewCombiner(s Suite, threshold, maxSigners uint) (*Combiner, error) {
if threshold > maxSigners {
return nil, errors.New("frost: invalid parameters")
}

return &Combiner{Suite: s, threshold: threshold, maxSigners: maxSigners}, nil
}

func (c Combiner) CheckSignShares(
signShares []*SignShare,
pubKeySigners []*PublicKey,
coms []*Commitment,
pubKeyGroup *PublicKey,
msg []byte,
) bool {
if l := len(signShares); !(int(c.threshold) < l && l <= int(c.maxSigners)) {
return false
}
if l := len(pubKeySigners); !(int(c.threshold) < l && l <= int(c.maxSigners)) {
return false
}
if l := len(coms); !(int(c.threshold) < l && l <= int(c.maxSigners)) {
return false
}

for i := range signShares {
if !signShares[i].Verify(c.Suite, pubKeySigners[i], coms[i], coms, pubKeyGroup, msg) {
return false
}
}

return true
}

func (c Combiner) Sign(msg []byte, coms []*Commitment, signShares []*SignShare) ([]byte, error) {
if l := len(coms); l <= int(c.threshold) {
return nil, fmt.Errorf("frost: only %v shares of %v required", l, c.threshold)
}

bindingFactors, err := c.Suite.getBindingFactors(coms, msg)
if err != nil {
return nil, err
}

groupCom, err := c.Suite.getGroupCommitment(coms, bindingFactors)
if err != nil {
return nil, err
}

gcEnc, err := groupCom.MarshalBinaryCompress()
if err != nil {
return nil, err
}

z := c.Suite.g.NewScalar()
for i := range signShares {
z.Add(z, signShares[i].s.Value)
}

zEnc, err := z.MarshalBinary()
if err != nil {
return nil, err
}

return append(append([]byte{}, gcEnc...), zEnc...), nil
}
134 changes: 134 additions & 0 deletions tss/frost/commit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package frost

import (
"errors"
"fmt"
"io"
"sort"

"github.com/cloudflare/circl/group"
)

type Nonce struct {
ID group.Scalar
hiding, binding group.Scalar
}

func (s Suite) nonceGenerate(rnd io.Reader, secret group.Scalar) (group.Scalar, error) {
randomBytes := make([]byte, 32)
_, err := io.ReadFull(rnd, randomBytes)
if err != nil {
return nil, err
}
secretEnc, err := secret.MarshalBinary()
if err != nil {
return nil, err
}

return s.hasher.h3(append(randomBytes, secretEnc...)), nil
}

type Commitment struct {
ID group.Scalar
hiding, binding group.Element
}

func (c Commitment) MarshalBinary() ([]byte, error) {
id, err := c.ID.MarshalBinary()
if err != nil {
return nil, err
}
h, err := c.hiding.MarshalBinaryCompress()
if err != nil {
return nil, err
}
b, err := c.binding.MarshalBinaryCompress()
if err != nil {
return nil, err
}

return append(append(id, h...), b...), nil
}

func encodeCommitments(coms []*Commitment) ([]byte, error) {
sort.SliceStable(coms, func(i, j int) bool {
return coms[i].ID.(fmt.Stringer).String() < coms[j].ID.(fmt.Stringer).String()
})

var out []byte
for i := range coms {
cEnc, err := coms[i].MarshalBinary()
if err != nil {
return nil, err
}
out = append(out, cEnc...)
}
return out, nil
}

type bindingFactor struct {
ID group.Scalar
factor group.Scalar
}

func (s Suite) getBindingFactorFromID(bindingFactors []bindingFactor, id group.Scalar) (group.Scalar, error) {
for i := range bindingFactors {
if bindingFactors[i].ID.IsEqual(id) {
return bindingFactors[i].factor, nil
}
}
return nil, errors.New("frost: id not found")
}

func (s Suite) getBindingFactors(coms []*Commitment, msg []byte) ([]bindingFactor, error) {
msgHash := s.hasher.h4(msg)
encodeComs, err := encodeCommitments(coms)
if err != nil {
return nil, err
}
encodeComsHash := s.hasher.h5(encodeComs)
rhoInputPrefix := append(msgHash, encodeComsHash...)

bindingFactors := make([]bindingFactor, len(coms))
for i := range coms {
id, err := coms[i].ID.MarshalBinary()
if err != nil {
return nil, err
}
rhoInput := append(append([]byte{}, rhoInputPrefix...), id...)
bf := s.hasher.h1(rhoInput)
bindingFactors[i] = bindingFactor{ID: coms[i].ID, factor: bf}
}

return bindingFactors, nil
}

func (s Suite) getGroupCommitment(coms []*Commitment, bindingFactors []bindingFactor) (group.Element, error) {
gc := s.g.NewElement()
tmp := s.g.NewElement()
for i := range coms {
bf, err := s.getBindingFactorFromID(bindingFactors, coms[i].ID)
if err != nil {
return nil, err
}
tmp.Mul(coms[i].binding, bf)
tmp.Add(tmp, coms[i].hiding)
gc.Add(gc, tmp)
}

return gc, nil
}

func (s Suite) getChallenge(groupCom group.Element, pubKey *PublicKey, msg []byte) (group.Scalar, error) {
gcEnc, err := groupCom.MarshalBinaryCompress()
if err != nil {
return nil, err
}
pkEnc, err := pubKey.key.MarshalBinaryCompress()
if err != nil {
return nil, err
}
chInput := append(append(append([]byte{}, gcEnc...), pkEnc...), msg...)

return s.hasher.h2(chInput), nil
}
96 changes: 96 additions & 0 deletions tss/frost/frost.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Package frost provides the FROST threshold signature scheme for Schnorr signatures.
//
// References
//
// FROST paper: https://eprint.iacr.org/2020/852
// draft-irtf-cfrg-frost: https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost
//
// Version supported: v11
package frost

import (
"io"

"github.com/cloudflare/circl/group"
"github.com/cloudflare/circl/secretsharing"
)

type PrivateKey struct {
Suite
key group.Scalar
pubKey *PublicKey
}

type PublicKey struct {
Suite
key group.Element
}

func GenerateKey(s Suite, rnd io.Reader) *PrivateKey {
return &PrivateKey{s, s.g.RandomNonZeroScalar(rnd), nil}
}

func (k *PrivateKey) Public() *PublicKey {
return &PublicKey{k.Suite, k.Suite.g.NewElement().MulGen(k.key)}
}

type SharesCommitment = []group.Element

func (k *PrivateKey) Split(rnd io.Reader, threshold, maxSigners uint) ([]PeerSigner, SharesCommitment, error) {
vss := secretsharing.NewFeldmanSecretSharing(threshold)
shares, sharesCom, err := vss.Shard(rnd, k.key, maxSigners)
if err != nil {
return nil, nil, err
}
peers := make([]PeerSigner, len(shares))
for i := range shares {
peers[i] = PeerSigner{
Suite: k.Suite,
threshold: uint16(threshold),
maxSigners: uint16(maxSigners),
keyShare: secretsharing.Share{
ID: shares[i].ID,
Value: shares[i].Value,
},
myPubKey: nil,
}
}

return peers, sharesCom, nil
}

func Verify(s Suite, pubKey *PublicKey, msg, signature []byte) bool {
params := s.g.Params()
Ne, Ns := params.CompressedElementLength, params.ScalarLength
if len(signature) < int(Ne+Ns) {
return false
}

REnc := signature[:Ne]
R := s.g.NewElement()
err := R.UnmarshalBinary(REnc)
if err != nil {
return false
}

zEnc := signature[Ne : Ne+Ns]
z := s.g.NewScalar()
err = z.UnmarshalBinary(zEnc)
if err != nil {
return false
}

pubKeyEnc, err := pubKey.key.MarshalBinaryCompress()
if err != nil {
return false
}

chInput := append(append(append([]byte{}, REnc...), pubKeyEnc...), msg...)
c := s.hasher.h2(chInput)

l := s.g.NewElement().MulGen(z)
r := s.g.NewElement().Mul(pubKey.key, c)
r.Add(r, R)

return l.IsEqual(r)
}
Loading

0 comments on commit a3638fe

Please sign in to comment.