diff --git a/tss/frost/combiner.go b/tss/frost/combiner.go new file mode 100644 index 00000000..488f5f9a --- /dev/null +++ b/tss/frost/combiner.go @@ -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 +} diff --git a/tss/frost/commit.go b/tss/frost/commit.go new file mode 100644 index 00000000..77ed1b91 --- /dev/null +++ b/tss/frost/commit.go @@ -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 +} diff --git a/tss/frost/frost.go b/tss/frost/frost.go new file mode 100644 index 00000000..c66f1069 --- /dev/null +++ b/tss/frost/frost.go @@ -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) +} diff --git a/tss/frost/frost_test.go b/tss/frost/frost_test.go new file mode 100644 index 00000000..4ca8b7f6 --- /dev/null +++ b/tss/frost/frost_test.go @@ -0,0 +1,153 @@ +package frost_test + +import ( + "crypto/rand" + "fmt" + "testing" + + "github.com/cloudflare/circl/internal/test" + "github.com/cloudflare/circl/tss/frost" +) + +func TestFrost(t *testing.T) { + for _, si := range []frost.Suite{frost.Ristretto255, frost.P256} { + t.Run(fmt.Sprintf("%v", si), func(tt *testing.T) { testFrost(tt, si) }) + } +} + +func testFrost(tt *testing.T, suite frost.Suite) { + t, n := uint(3), uint(5) + + privKey := frost.GenerateKey(suite, rand.Reader) + pubKeyGroup := privKey.Public() + peers, keyShareCommits, err := privKey.Split(rand.Reader, t, n) + test.CheckNoErr(tt, err, "failed to split secret") + + // every peer can validate its own keyShare. + for i := range peers { + valid := peers[i].CheckKeyShare(keyShareCommits) + test.CheckOk(valid == true, "invalid key share", tt) + } + + // Only k peers try to generate a signature. + for k := uint(0); k < n; k++ { + // round 1 + nonces := make([]*frost.Nonce, k) + commits := make([]*frost.Commitment, k) + pkSigners := make([]*frost.PublicKey, k) + for i := range peers[:k] { + nonces[i], commits[i], err = peers[i].Commit(rand.Reader) + test.CheckNoErr(tt, err, "failed to commit") + pkSigners[i] = peers[i].Public() + } + + // round 2 + msg := []byte("it's cold here") + signShares := make([]*frost.SignShare, k) + for i := range peers[:k] { + signShares[i], err = peers[i].Sign(msg, pubKeyGroup, nonces[i], commits) + test.CheckNoErr(tt, err, "failed to create a sign share") + } + + // Combiner + combiner, err := frost.NewCombiner(suite, t, n) + test.CheckNoErr(tt, err, "failed to create combiner") + + valid := combiner.CheckSignShares(signShares, pkSigners, commits, pubKeyGroup, msg) + if k > t { + test.CheckOk(valid == true, "invalid sign shares", tt) + } else { + test.CheckOk(valid == false, "must be invalid sign shares", tt) + } + + signature, err := combiner.Sign(msg, commits, signShares) + if k > t { + test.CheckNoErr(tt, err, "failed to produce signature") + // anyone can verify + valid := frost.Verify(suite, pubKeyGroup, msg, signature) + test.CheckOk(valid == true, "invalid signature", tt) + } else { + test.CheckIsErr(tt, err, "should not produce a signature") + test.CheckOk(signature == nil, "not nil signature", tt) + } + } +} + +func BenchmarkFrost(b *testing.B) { + for _, si := range []frost.Suite{frost.Ristretto255, frost.P256} { + b.Run(fmt.Sprintf("%v", si), func(bb *testing.B) { benchmarkFrost(bb, si) }) + } +} + +func benchmarkFrost(b *testing.B, suite frost.Suite) { + t, n := uint(3), uint(5) + + privKey := frost.GenerateKey(suite, rand.Reader) + peers, keyShareCommits, err := privKey.Split(rand.Reader, t, n) + test.CheckNoErr(b, err, "failed to split secret") + + b.Run("SplitKey", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _, _ = privKey.Split(rand.Reader, t, n) + } + }) + + pubKeyGroup := privKey.Public() + msg := []byte("it's cold here") + + nonces := make([]*frost.Nonce, len(peers)) + commits := make([]*frost.Commitment, len(peers)) + pkSigners := make([]*frost.PublicKey, len(peers)) + for i := range peers { + nonces[i], commits[i], err = peers[i].Commit(rand.Reader) + test.CheckNoErr(b, err, "failed to commit") + pkSigners[i] = peers[i].Public() + } + + b.Run("CheckKeyShare", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = peers[0].CheckKeyShare(keyShareCommits) + } + }) + + b.Run("Commit", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _, _ = peers[0].Commit(rand.Reader) + } + }) + + b.Run("SignShare", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = peers[0].Sign(msg, pubKeyGroup, nonces[0], commits) + } + }) + + signShares := make([]*frost.SignShare, len(peers)) + for i := range peers { + signShares[i], err = peers[i].Sign(msg, pubKeyGroup, nonces[i], commits) + test.CheckNoErr(b, err, "failed to create a sign share") + } + + combiner, err := frost.NewCombiner(suite, t, n) + test.CheckNoErr(b, err, "failed to create combiner") + + b.Run("CheckSignShares", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = combiner.CheckSignShares(signShares, pkSigners, commits, pubKeyGroup, msg) + } + }) + + b.Run("Sign", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = combiner.Sign(msg, commits, signShares) + } + }) + + b.Run("Verify", func(b *testing.B) { + signature, _ := combiner.Sign(msg, commits, signShares) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = frost.Verify(suite, pubKeyGroup, msg, signature) + } + }) +} diff --git a/tss/frost/peer.go b/tss/frost/peer.go new file mode 100644 index 00000000..d9c6adab --- /dev/null +++ b/tss/frost/peer.go @@ -0,0 +1,162 @@ +package frost + +import ( + "errors" + "fmt" + "io" + "sort" + + "github.com/cloudflare/circl/group" + "github.com/cloudflare/circl/math/polynomial" + "github.com/cloudflare/circl/secretsharing" +) + +type PeerSigner struct { + Suite + threshold uint16 + maxSigners uint16 + keyShare secretsharing.Share + myPubKey *PublicKey +} + +func (p PeerSigner) Commit(rnd io.Reader) (*Nonce, *Commitment, error) { + hidingNonce, err := p.Suite.nonceGenerate(rnd, p.keyShare.Value) + if err != nil { + return nil, nil, err + } + bindingNonce, err := p.Suite.nonceGenerate(rnd, p.keyShare.Value) + if err != nil { + return nil, nil, err + } + + return p.commitWithNonce(hidingNonce, bindingNonce) +} + +func (p PeerSigner) commitWithNonce(hidingNonce, bindingNonce group.Scalar) (*Nonce, *Commitment, error) { + hidingNonceCom := p.Suite.g.NewElement().MulGen(hidingNonce) + bindingNonceCom := p.Suite.g.NewElement().MulGen(bindingNonce) + return &Nonce{p.keyShare.ID, hidingNonce, bindingNonce}, &Commitment{p.keyShare.ID, hidingNonceCom, bindingNonceCom}, nil +} + +func (p PeerSigner) CheckKeyShare(shareCom SharesCommitment) bool { + vss := secretsharing.NewFeldmanSecretSharing(uint(p.threshold)) + return vss.Verify(p.keyShare, shareCom) +} + +func (p PeerSigner) Public() *PublicKey { + if p.myPubKey == nil { + p.myPubKey = &PublicKey{p.Suite, p.Suite.g.NewElement().MulGen(p.keyShare.Value)} + } + return p.myPubKey +} + +func (p PeerSigner) Sign(msg []byte, pubKey *PublicKey, nonce *Nonce, coms []*Commitment) (*SignShare, error) { + if !p.keyShare.ID.IsEqual(nonce.ID) { + return nil, errors.New("frost: bad id") + } + aux, err := p.Suite.common(p.keyShare.ID, msg, pubKey, coms) + if err != nil { + return nil, err + } + + tmp := p.Suite.g.NewScalar().Mul(nonce.binding, aux.bindingFactor) + signShare := p.Suite.g.NewScalar().Add(nonce.hiding, tmp) + tmp.Mul(aux.lambdaID, p.keyShare.Value) + tmp.Mul(tmp, aux.challenge) + signShare.Add(signShare, tmp) + + return &SignShare{s: secretsharing.Share{ + ID: p.keyShare.ID, + Value: signShare, + }}, nil +} + +type SignShare struct { + s secretsharing.Share +} + +func (s *SignShare) Verify( + suite Suite, + pubKeySigner *PublicKey, + comSigner *Commitment, + coms []*Commitment, + pubKeyGroup *PublicKey, + msg []byte, +) bool { + if s.s.ID != comSigner.ID || s.s.ID.IsZero() { + return false + } + + aux, err := suite.common(s.s.ID, msg, pubKeyGroup, coms) + if err != nil { + return false + } + + comShare := suite.g.NewElement().Mul(coms[aux.idx].binding, aux.bindingFactor) + comShare.Add(comShare, coms[aux.idx].hiding) + + l := suite.g.NewElement().MulGen(s.s.Value) + r := suite.g.NewElement().Mul(pubKeySigner.key, suite.g.NewScalar().Mul(aux.challenge, aux.lambdaID)) + r.Add(r, comShare) + + return l.IsEqual(r) +} + +type commonAux struct { + idx uint + lambdaID group.Scalar + challenge group.Scalar + bindingFactor group.Scalar +} + +func (s Suite) common(id group.Scalar, msg []byte, pubKey *PublicKey, coms []*Commitment) (aux *commonAux, err error) { + if !sort.SliceIsSorted(coms, + func(i, j int) bool { + return coms[i].ID.(fmt.Stringer).String() < coms[j].ID.(fmt.Stringer).String() + }, + ) { + return nil, errors.New("frost: commitments must be sorted") + } + + idx := sort.Search(len(coms), func(j int) bool { + return coms[j].ID.(fmt.Stringer).String() >= id.(fmt.Stringer).String() + }) + if !(idx < len(coms) && coms[idx].ID.IsEqual(id)) { + return nil, errors.New("frost: commitment not present") + } + + bindingFactors, err := s.getBindingFactors(coms, msg) + if err != nil { + return nil, err + } + + bindingFactor, err := s.getBindingFactorFromID(bindingFactors, id) + if err != nil { + return nil, err + } + + groupCom, err := s.getGroupCommitment(coms, bindingFactors) + if err != nil { + return nil, err + } + + challenge, err := s.getChallenge(groupCom, pubKey, msg) + if err != nil { + return nil, err + } + + peers := make([]group.Scalar, len(coms)) + for i := range coms { + peers[i] = coms[i].ID.Copy() + } + + zero := s.g.NewScalar() + lambdaID := polynomial.LagrangeBase(uint(idx), peers, zero) + + return &commonAux{ + idx: uint(idx), + lambdaID: lambdaID, + challenge: challenge, + bindingFactor: bindingFactor, + }, nil +} diff --git a/tss/frost/suites.go b/tss/frost/suites.go new file mode 100644 index 00000000..f4f91717 --- /dev/null +++ b/tss/frost/suites.go @@ -0,0 +1,77 @@ +package frost + +import ( + "crypto" + _ "crypto/sha256" // added to link library. + _ "crypto/sha512" // added to link library. + "fmt" + + r255 "github.com/bwesterb/go-ristretto" + "github.com/cloudflare/circl/group" +) + +var ( + P256 = Suite{group.P256, suiteP{group.P256, suiteCommon{crypto.SHA256, "FROST-P256-SHA256-v11"}}} + Ristretto255 = Suite{group.Ristretto255, suiteRis255{suiteCommon{crypto.SHA512, "FROST-RISTRETTO255-SHA512-v11"}}} +) + +type Suite struct { + g group.Group + hasher interface { + h1(m []byte) group.Scalar + h2(m []byte) group.Scalar + h3(m []byte) group.Scalar + h4(m []byte) []byte + h5(m []byte) []byte + } +} + +func (s Suite) String() string { return s.hasher.(fmt.Stringer).String() } + +const ( + labelRho = "rho" + labelChal = "chal" + labelNonce = "nonce" + labelMsg = "msg" + labelCom = "com" +) + +type suiteCommon struct { + hash crypto.Hash + context string +} + +func (s suiteCommon) String() string { return s.context[:len(s.context)-4] } +func (s suiteCommon) h4(m []byte) []byte { return s.hashLabeled(labelMsg, m) } +func (s suiteCommon) h5(m []byte) []byte { return s.hashLabeled(labelCom, m) } +func (s suiteCommon) hashLabeled(label string, m []byte) []byte { + H := s.hash.New() + _, _ = H.Write([]byte(s.context + label)) + _, _ = H.Write(m) + return H.Sum(nil) +} + +type suiteP struct { + g group.Group + suiteCommon +} + +func (s suiteP) h1(m []byte) group.Scalar { return s.g.HashToScalar(m, []byte(s.context+labelRho)) } +func (s suiteP) h2(m []byte) group.Scalar { return s.g.HashToScalar(m, []byte(s.context+labelChal)) } +func (s suiteP) h3(m []byte) group.Scalar { return s.g.HashToScalar(m, []byte(s.context+labelNonce)) } + +type suiteRis255 struct{ suiteCommon } + +func (s suiteRis255) getScalar(input []byte) group.Scalar { + var data [64]byte + copy(data[:], input[:64]) + y := new(r255.Scalar).SetReduced(&data) + bytes, _ := y.MarshalBinary() + z := group.Ristretto255.NewScalar() + _ = z.UnmarshalBinary(bytes) + return z +} + +func (s suiteRis255) h1(m []byte) group.Scalar { return s.getScalar(s.hashLabeled(labelRho, m)) } +func (s suiteRis255) h2(m []byte) group.Scalar { return s.getScalar(s.hashLabeled(labelChal, m)) } +func (s suiteRis255) h3(m []byte) group.Scalar { return s.getScalar(s.hashLabeled(labelNonce, m)) } diff --git a/tss/frost/testdata/frost_p256_sha256.json b/tss/frost/testdata/frost_p256_sha256.json new file mode 100644 index 00000000..5b610652 --- /dev/null +++ b/tss/frost/testdata/frost_p256_sha256.json @@ -0,0 +1,68 @@ +{ + "config": { + "MAX_PARTICIPANTS": "3", + "NUM_PARTICIPANTS": "2", + "MIN_PARTICIPANTS": "2", + "name": "FROST(P-256, SHA-256)", + "group": "P-256", + "hash": "SHA-256" + }, + "inputs": { + "group_secret_key": "8ba9bba2e0fd8c4767154d35a0b7562244a4aaf6f36c8fb8735fa48b301bd8de", + "group_public_key": "023a309ad94e9fe8a7ba45dfc58f38bf091959d3c99cfbd02b4dc00585ec45ab70", + "message": "74657374", + "share_polynomial_coefficients": [ + "80f25e6c0709353e46bfbe882a11bdbb1f8097e46340eb8673b7e14556e6c3a4" + ], + "participants": { + "1": { + "participant_share": "0c9c1a0fe806c184add50bbdcac913dda73e482daf95dcb9f35dbb0d8a9f7731" + }, + "2": { + "participant_share": "8d8e787bef0ff6c2f494ca45f4dad198c6bee01212d6c84067159c52e1863ad5" + }, + "3": { + "participant_share": "0e80d6e8f6192c003b5488ce1eec8f5429587d48cf001541e713b2d53c09d928" + } + } + }, + "round_one_outputs": { + "participant_list": "1,3", + "participants": { + "1": { + "hiding_nonce_randomness": "96be177390d40df347f7e0c5fceab4724dc41bc595ad822366be90f15f983a7b", + "binding_nonce_randomness": "74e2bf636456d87ce76a7429088998d81ae2faa5c8ce04e9bac5197d893adc06", + "hiding_nonce": "2e60c16b89c17d1cefef5e7f3a8c8f6ae821b57d1e040da647f8280cce739198", + "binding_nonce": "2c07e805df65c86ba4ede8edcc222e2a5d7b1ee3ba4bc11c68ed8d1307de40e6", + "hiding_nonce_commitment": "03893a951d667965ea784f8a715a6b6337e3a434871101502f6dc66066ab37b97c", + "binding_nonce_commitment": "0272dc56a3c0fe1454ea8355c6b6c38d061c166426ff095aab25fecd32af9961a8", + "binding_factor_input": "350c8b523feea9bb35720e9fbe0405ed48d78caa4fb60869f34367e144c68bb02da00e7af1d696811fd38187745717e96978f97e4ec287818a7afede5435b0280000000000000000000000000000000000000000000000000000000000000001", + "binding_factor": "38ac5d4ce49ea2a403279278fabf3b3275eef91c86f3b83355b6975777d3acd2" + }, + "3": { + "hiding_nonce_randomness": "65f081e0ae1f83ca6bb792d050bc0ac23728f617341f9532fcbd90ee5b3bd3e5", + "binding_nonce_randomness": "3613b6b5ac6833848c52e2e3c27ba777b4b79160af667670315e87ed5df47364", + "hiding_nonce": "8c56d9e1bbb2792a8761d9c9e108e12957acd3840b69ad3307a24ac5e9618875", + "binding_nonce": "2df2d0828c9a7fe9f338b3bb885c3a6aa87fda81cd75b11c4b1275c741bd68e6", + "hiding_nonce_commitment": "02ef00cb2527c5f363b5d90c15b7dc6b3fd3c38decca715d8feea6c0642c38d366", + "binding_nonce_commitment": "0244f86440396661bd1d9cab34e72543e1cccb6768a7f445099f3d6d8719d84a66", + "binding_factor_input": "350c8b523feea9bb35720e9fbe0405ed48d78caa4fb60869f34367e144c68bb02da00e7af1d696811fd38187745717e96978f97e4ec287818a7afede5435b0280000000000000000000000000000000000000000000000000000000000000003", + "binding_factor": "11d9b0267c04e79f44615ed7d1fe7117f3d5b802351ace82f51454443a1efca9" + } + } + }, + "round_two_outputs": { + "participant_list": "1,3", + "participants": { + "1": { + "sig_share": "dd18c4b0e3cc1ed70a8274bafa6b6b65cd46d87e52070078e3d684bbe0946fca" + }, + "3": { + "sig_share": "0644c5aaa8f27b4bf39dda8a9dcbd90dbcbb358c15c165c762997c3dd51a3b09" + } + } + }, + "final_output": { + "sig": "03289283518ddc78ad2d53418245e4f83c1eaeb523e0abd3823655f32997990177e35d8a5b8cbe9a22fe204f45983744738a020e0a67c86640467000f9b5aeaad3" + } +} \ No newline at end of file diff --git a/tss/frost/testdata/frost_ristretto255_sha512.json b/tss/frost/testdata/frost_ristretto255_sha512.json new file mode 100644 index 00000000..979d4ff9 --- /dev/null +++ b/tss/frost/testdata/frost_ristretto255_sha512.json @@ -0,0 +1,68 @@ +{ + "config": { + "MAX_PARTICIPANTS": "3", + "NUM_PARTICIPANTS": "2", + "MIN_PARTICIPANTS": "2", + "name": "FROST(ristretto255, SHA-512)", + "group": "ristretto255", + "hash": "SHA-512" + }, + "inputs": { + "group_secret_key": "1b25a55e463cfd15cf14a5d3acc3d15053f08da49c8afcf3ab265f2ebc4f970b", + "group_public_key": "e2a62f39eede11269e3bd5a7d97554f5ca384f9f6d3dd9c3c0d05083c7254f57", + "message": "74657374", + "share_polynomial_coefficients": [ + "410f8b744b19325891d73736923525a4f596c805d060dfb9c98009d34e3fec02" + ], + "participants": { + "1": { + "participant_share": "5c3430d391552f6e60ecdc093ff9f6f4488756aa6cebdbad75a768010b8f830e" + }, + "2": { + "participant_share": "b06fc5eac20b4f6e1b271d9df2343d843e1e1fb03c4cbb673f2872d459ce6f01" + }, + "3": { + "participant_share": "f17e505f0e2581c6acfe54d3846a622834b5e7b50cad9a2109a97ba7a80d5c04" + } + } + }, + "round_one_outputs": { + "participant_list": "1,3", + "participants": { + "1": { + "hiding_nonce_randomness": "705bef89bc7fb7b9fdd6ccf475643c37638aa2a36b87676c9045d0bce36711a7", + "binding_nonce_randomness": "7fe0a8455ccbcd1fc58598e04ba08759e1aed04e46b79c02d0f7dc259ff42db4", + "hiding_nonce": "c1e56ae9bfceb570c4ef2dd10cdb79fd887b326093da3fa7eba7320caf284a03", + "binding_nonce": "8159b8b194083d315fdbeac3cfbc3c46adcf5963e72b5bb5b1956e228b20030d", + "hiding_nonce_commitment": "ce43378a940f4f576ffaea183ba7815ace0d91a50e2b80b05766072a22c26702", + "binding_nonce_commitment": "805bd85f1eefad8d248cf98a149491c0a6bac3f4c13e9709c547e9b704711a52", + "binding_factor_input": "9c245d5fc2e451c5c5a617cc6f2a20629fb317d9b1c1915ab4bfa319d4ebf922c54dd1a5b3b754550c72734ac9255db8107a2b01f361754d9f13f428c2f6de9ebad59fa100eebaaeb77f179d097b36ade3bd13f02214f4394ead67be3d69bc031b1ca7332b1e5dcef9aae4cc80af30872a9c48ad92ecbee52b4bb76759c4794d0100000000000000000000000000000000000000000000000000000000000000", + "binding_factor": "3f02ce577740f60ea7bf30f6b1c891738e2b810b3754a756b15d94858ea5aa0f" + }, + "3": { + "hiding_nonce_randomness": "812e3907ec1748e5663192ba1b8bcef999534c465914dd8239c3162330d3b0b9", + "binding_nonce_randomness": "a6dc55712d9dd4b3ef5d22aed36169841a6758aea7cb707e7929d8c8cda3e80b", + "hiding_nonce": "c645ac5632f4658bd560f60e7c3aeb40a868267d436037cb2ef35efef6d0a809", + "binding_nonce": "54c933a872de06fe50933596f99e019b2d8aeaec087f9d9459d8583acccec101", + "hiding_nonce_commitment": "088d7e22d0900358043480f60be82f06538798b6fe63c958c308996199c02e19", + "binding_nonce_commitment": "faf53f8c449fafd3d2d37c81fed72b59bed56d090674ebbc629aab14cedc0a2b", + "binding_factor_input": "9c245d5fc2e451c5c5a617cc6f2a20629fb317d9b1c1915ab4bfa319d4ebf922c54dd1a5b3b754550c72734ac9255db8107a2b01f361754d9f13f428c2f6de9ebad59fa100eebaaeb77f179d097b36ade3bd13f02214f4394ead67be3d69bc031b1ca7332b1e5dcef9aae4cc80af30872a9c48ad92ecbee52b4bb76759c4794d0300000000000000000000000000000000000000000000000000000000000000", + "binding_factor": "4d73c0a3a763931becb5bbd2a977cb0c9341166f1693b5c057433ae877ed2008" + } + } + }, + "round_two_outputs": { + "participant_list": "1,3", + "participants": { + "1": { + "sig_share": "735083b8fdd2aee8a892c26ce8985e25bb0666fa8e4234827801fe4cc7be1f0c" + }, + "3": { + "sig_share": "1b44ebcc5807967156035d5e21b6b0dbbf2f23b72dea9d69f4b6576a67e6000d" + } + } + }, + "final_output": { + "sig": "ee4e7905c52e72b8d6acb2f015fd499066fead78c7f696e817212d16c134fd66a1c078283c77320229f927282b5530ec7a3689b1bc2cd2eb6cb855b72ea52009" + } +} \ No newline at end of file diff --git a/tss/frost/vectors_test.go b/tss/frost/vectors_test.go new file mode 100644 index 00000000..62b5f02c --- /dev/null +++ b/tss/frost/vectors_test.go @@ -0,0 +1,237 @@ +package frost + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "os" + "testing" + + "github.com/cloudflare/circl/group" + "github.com/cloudflare/circl/internal/test" + "github.com/cloudflare/circl/secretsharing" +) + +type vector struct { + Config struct { + MAXPARTICIPANTS uint16 `json:"MAX_PARTICIPANTS,string"` + NUMPARTICIPANTS uint16 `json:"NUM_PARTICIPANTS,string"` + MINPARTICIPANTS uint16 `json:"MIN_PARTICIPANTS,string"` + Name string `json:"name"` + Group string `json:"group"` + Hash string `json:"hash"` + } `json:"config"` + Inputs struct { + GroupSecretKey string `json:"group_secret_key"` + GroupPublicKey string `json:"group_public_key"` + Message string `json:"message"` + SharePolynomialCoefficients []string `json:"share_polynomial_coefficients"` + Participants struct { + Num1 struct { + ParticipantShare string `json:"participant_share"` + } `json:"1"` + Num2 struct { + ParticipantShare string `json:"participant_share"` + } `json:"2"` + Num3 struct { + ParticipantShare string `json:"participant_share"` + } `json:"3"` + } `json:"participants"` + } `json:"inputs"` + RoundOneOutputs struct { + ParticipantList string `json:"participant_list"` + Participants struct { + Num1 struct { + HidingNonceRandomness string `json:"hiding_nonce_randomness"` + BindingNonceRandomness string `json:"binding_nonce_randomness"` + HidingNonce string `json:"hiding_nonce"` + BindingNonce string `json:"binding_nonce"` + HidingNonceCommitment string `json:"hiding_nonce_commitment"` + BindingNonceCommitment string `json:"binding_nonce_commitment"` + BindingFactorInput string `json:"binding_factor_input"` + BindingFactor string `json:"binding_factor"` + } `json:"1"` + Num3 struct { + HidingNonceRandomness string `json:"hiding_nonce_randomness"` + BindingNonceRandomness string `json:"binding_nonce_randomness"` + HidingNonce string `json:"hiding_nonce"` + BindingNonce string `json:"binding_nonce"` + HidingNonceCommitment string `json:"hiding_nonce_commitment"` + BindingNonceCommitment string `json:"binding_nonce_commitment"` + BindingFactorInput string `json:"binding_factor_input"` + BindingFactor string `json:"binding_factor"` + } `json:"3"` + } `json:"participants"` + } `json:"round_one_outputs"` + RoundTwoOutputs struct { + ParticipantList string `json:"participant_list"` + Participants struct { + Num1 struct { + SigShare string `json:"sig_share"` + } `json:"1"` + Num3 struct { + SigShare string `json:"sig_share"` + } `json:"3"` + } `json:"participants"` + } `json:"round_two_outputs"` + FinalOutput struct { + Sig string `json:"sig"` + } `json:"final_output"` +} + +func fromHex(t *testing.T, s, errMsg string) []byte { + t.Helper() + bytes, err := hex.DecodeString(s) + test.CheckNoErr(t, err, "decoding "+errMsg) + + return bytes +} + +func toBytesScalar(t *testing.T, s group.Scalar) []byte { + t.Helper() + bytes, err := s.MarshalBinary() + test.CheckNoErr(t, err, "decoding scalar") + + return bytes +} + +func toBytesElt(t *testing.T, e group.Element) []byte { + t.Helper() + bytes, err := e.MarshalBinaryCompress() + test.CheckNoErr(t, err, "decoding element") + + return bytes +} + +func toScalar(t *testing.T, g group.Group, s, errMsg string) group.Scalar { + t.Helper() + r := g.NewScalar() + rBytes := fromHex(t, s, errMsg) + err := r.UnmarshalBinary(rBytes) + test.CheckNoErr(t, err, errMsg) + + return r +} + +func compareBytes(t *testing.T, got, want []byte) { + t.Helper() + if !bytes.Equal(got, want) { + test.ReportError(t, got, want) + } +} + +func (v *vector) test(t *testing.T, suite Suite) { + privKey := &PrivateKey{suite, toScalar(t, suite.g, v.Inputs.GroupSecretKey, "bad private key"), nil} + pubKeyGroup := privKey.Public() + compareBytes(t, toBytesElt(t, pubKeyGroup.key), fromHex(t, v.Inputs.GroupPublicKey, "bad public key")) + + p1 := PeerSigner{ + Suite: suite, + threshold: v.Config.NUMPARTICIPANTS, + maxSigners: v.Config.MAXPARTICIPANTS, + keyShare: secretsharing.Share{ + ID: suite.g.NewScalar().SetUint64(1), + Value: toScalar(t, suite.g, v.Inputs.Participants.Num1.ParticipantShare, "signer share value"), + }, + myPubKey: nil, + } + + /*p2 := PeerSigner{ + Suite: suite, + threshold: v.Config.NUMPARTICIPANTS, + maxSigners: v.Config.MAXPARTICIPANTS, + keyShare: secretsharing.Share{ + ID: suite.g.NewScalar().SetUint64(2), + Value: toScalar(t, suite.g, v.Inputs.Participants.Num2.ParticipantShare, "signer share value"), + }, + myPubKey: nil, + }*/ + + p3 := PeerSigner{ + Suite: suite, + threshold: v.Config.NUMPARTICIPANTS, + maxSigners: v.Config.MAXPARTICIPANTS, + keyShare: secretsharing.Share{ + ID: suite.g.NewScalar().SetUint64(3), + Value: toScalar(t, suite.g, v.Inputs.Participants.Num3.ParticipantShare, "signer share value"), + }, + myPubKey: nil, + } + + hn1 := toScalar(t, suite.g, v.RoundOneOutputs.Participants.Num1.HidingNonce, "hiding nonce") + bn1 := toScalar(t, suite.g, v.RoundOneOutputs.Participants.Num1.BindingNonce, "binding nonce") + nonce1, commit1, err := p1.commitWithNonce(hn1, bn1) + test.CheckNoErr(t, err, "failed to commit") + + compareBytes(t, toBytesElt(t, commit1.hiding), fromHex(t, v.RoundOneOutputs.Participants.Num1.HidingNonceCommitment, "hiding nonce commit")) + compareBytes(t, toBytesElt(t, commit1.binding), fromHex(t, v.RoundOneOutputs.Participants.Num1.BindingNonceCommitment, "binding nonce commit")) + + hn3 := toScalar(t, suite.g, v.RoundOneOutputs.Participants.Num3.HidingNonce, "hiding nonce") + bn3 := toScalar(t, suite.g, v.RoundOneOutputs.Participants.Num3.BindingNonce, "binding nonce") + nonce3, commit3, err := p3.commitWithNonce(hn3, bn3) + test.CheckNoErr(t, err, "failed to commit") + + compareBytes(t, toBytesElt(t, commit3.hiding), fromHex(t, v.RoundOneOutputs.Participants.Num3.HidingNonceCommitment, "hiding nonce commit")) + compareBytes(t, toBytesElt(t, commit3.binding), fromHex(t, v.RoundOneOutputs.Participants.Num3.BindingNonceCommitment, "binding nonce commit")) + + msg := fromHex(t, v.Inputs.Message, "bad msg") + commits := []*Commitment{commit1, commit3} + bindingFactors, err := suite.getBindingFactors(commits, msg) + test.CheckNoErr(t, err, "failed to get binding factors") + + compareBytes(t, toBytesScalar(t, bindingFactors[0].factor), fromHex(t, v.RoundOneOutputs.Participants.Num1.BindingFactor, "binding factor")) + compareBytes(t, toBytesScalar(t, bindingFactors[1].factor), fromHex(t, v.RoundOneOutputs.Participants.Num3.BindingFactor, "binding factor")) + + signShares1, err := p1.Sign(msg, pubKeyGroup, nonce1, commits) + test.CheckNoErr(t, err, "failed to sign share") + compareBytes(t, toBytesScalar(t, signShares1.s.Value), fromHex(t, v.RoundTwoOutputs.Participants.Num1.SigShare, "sign share")) + + signShares3, err := p3.Sign(msg, pubKeyGroup, nonce3, commits) + test.CheckNoErr(t, err, "failed to sign share") + compareBytes(t, toBytesScalar(t, signShares3.s.Value), fromHex(t, v.RoundTwoOutputs.Participants.Num3.SigShare, "sign share")) + + combiner, err := NewCombiner(suite, uint(v.Config.MINPARTICIPANTS-1), uint(v.Config.MAXPARTICIPANTS)) + test.CheckNoErr(t, err, "failed to create combiner") + + signShares := []*SignShare{signShares1, signShares3} + signature, err := combiner.Sign(msg, commits, signShares) + test.CheckNoErr(t, err, "failed to create signature") + compareBytes(t, signature, fromHex(t, v.FinalOutput.Sig, "signature")) + + valid := Verify(suite, pubKeyGroup, msg, signature) + test.CheckOk(valid == true, "invalid signature", t) +} + +func readFile(t *testing.T, fileName string) *vector { + t.Helper() + jsonFile, err := os.Open(fileName) + if err != nil { + t.Fatalf("File %v can not be opened. Error: %v", fileName, err) + } + defer jsonFile.Close() + input, err := io.ReadAll(jsonFile) + if err != nil { + t.Fatalf("File %v can not be read. Error: %v", fileName, err) + } + + var v vector + err = json.Unmarshal(input, &v) + if err != nil { + t.Fatalf("File %v can not be loaded. Error: %v", fileName, err) + } + + return &v +} + +func TestVectors(t *testing.T) { + // Draft published at https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-frost-11 + // Test vectors at https://github.com/cfrg/draft-irtf-cfrg-frost + // Version supported: v11 + suite, vector := P256, readFile(t, "testdata/frost_p256_sha256.json") + t.Run(fmt.Sprintf("%v", suite), func(tt *testing.T) { vector.test(tt, suite) }) + + suite, vector = Ristretto255, readFile(t, "testdata/frost_ristretto255_sha512.json") + t.Run(fmt.Sprintf("%v", suite), func(tt *testing.T) { vector.test(tt, suite) }) +} diff --git a/tss/tss.go b/tss/tss.go new file mode 100644 index 00000000..f3e650b4 --- /dev/null +++ b/tss/tss.go @@ -0,0 +1,2 @@ +// Package tss provides threshold signature schemes. +package tss