diff --git a/tss/frost/combiner.go b/tss/frost/combiner.go new file mode 100644 index 000000000..1ba4b41aa --- /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].share) + } + + 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 000000000..9989a31d2 --- /dev/null +++ b/tss/frost/commit.go @@ -0,0 +1,128 @@ +package frost + +import ( + "encoding/binary" + "errors" + "io" + "sort" + + "github.com/cloudflare/circl/group" +) + +type Nonce struct { + ID uint16 + hiding, binding group.Scalar +} + +func (s Suite) nonceGenerate(rnd io.Reader, secret group.Scalar) (group.Scalar, error) { + k := make([]byte, 32) + _, err := io.ReadFull(rnd, k) + if err != nil { + return nil, err + } + secretEnc, err := secret.MarshalBinary() + if err != nil { + return nil, err + } + + return s.hasher.h4(append(append([]byte{}, k...), secretEnc...)), nil +} + +type Commitment struct { + ID uint16 + hiding, binding group.Element +} + +func (c Commitment) MarshalBinary() ([]byte, error) { + bytes := (&[2]byte{})[:] + binary.BigEndian.PutUint16(bytes, c.ID) + + 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(bytes, h...), b...), nil +} + +func encodeComs(coms []*Commitment) ([]byte, error) { + sort.SliceStable(coms, func(i, j int) bool { return coms[i].ID < coms[j].ID }) + + 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 uint16 + factor group.Scalar +} + +func (s Suite) getBindingFactorFromID(bindingFactors []bindingFactor, id uint) (group.Scalar, error) { + for i := range bindingFactors { + if uint(bindingFactors[i].ID) == 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.h3(msg) + commitEncoded, err := encodeComs(coms) + if err != nil { + return nil, err + } + commitEncodedHash := s.hasher.h3(commitEncoded) + rhoInputPrefix := append(append([]byte{}, msgHash...), commitEncodedHash...) + + bindingFactors := make([]bindingFactor, len(coms)) + id := (&[2]byte{})[:] + for i := range coms { + binary.BigEndian.PutUint16(id, coms[i].ID) + bf := s.hasher.h1(append(append([]byte{}, rhoInputPrefix...), id...)) + 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, uint(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 000000000..2b7e2db9f --- /dev/null +++ b/tss/frost/frost.go @@ -0,0 +1,91 @@ +// 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 +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, err := secretsharing.NewVerifiable(k.Suite.g, threshold, maxSigners) + if err != nil { + return nil, nil, err + } + shares, sharesCom := vss.Shard(rnd, k.key) + peers := make([]PeerSigner, len(shares)) + for i := range shares { + peers[i] = PeerSigner{ + Suite: k.Suite, + ID: uint16(shares[i].ID), + threshold: uint16(threshold), + maxSigners: uint16(maxSigners), + keyShare: shares[i].Share, + 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 000000000..4ca8b7f63 --- /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 000000000..7f63d54d9 --- /dev/null +++ b/tss/frost/peer.go @@ -0,0 +1,158 @@ +package frost + +import ( + "errors" + "io" + "sort" + + "github.com/cloudflare/circl/group" + "github.com/cloudflare/circl/math/polynomial" + "github.com/cloudflare/circl/secretsharing" +) + +type PeerSigner struct { + Suite + ID uint16 + threshold uint16 + maxSigners uint16 + keyShare group.Scalar + myPubKey *PublicKey +} + +func (p PeerSigner) Commit(rnd io.Reader) (*Nonce, *Commitment, error) { + hidingNonce, err := p.Suite.nonceGenerate(rnd, p.keyShare) + if err != nil { + return nil, nil, err + } + bindingNonce, err := p.Suite.nonceGenerate(rnd, p.keyShare) + 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.ID, hidingNonce, bindingNonce}, &Commitment{p.ID, hidingNonceCom, bindingNonceCom}, nil +} + +func (p PeerSigner) CheckKeyShare(shareCom SharesCommitment) bool { + vss, err := secretsharing.NewVerifiable(p.Suite.g, uint(p.threshold), uint(p.maxSigners)) + if err != nil { + return false + } + return vss.Verify(secretsharing.Share{ID: uint(p.ID), Share: p.keyShare}, shareCom) +} + +func (p PeerSigner) Public() *PublicKey { + if p.myPubKey == nil { + p.myPubKey = &PublicKey{p.Suite, p.Suite.g.NewElement().MulGen(p.keyShare)} + } + return p.myPubKey +} + +func (p PeerSigner) Sign(msg []byte, pubKey *PublicKey, nonce *Nonce, coms []*Commitment) (*SignShare, error) { + if p.ID != nonce.ID { + return nil, errors.New("frost: bad id") + } + aux, err := p.Suite.common(uint(p.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) + tmp.Mul(tmp, aux.challenge) + signShare.Add(signShare, tmp) + + return &SignShare{ID: p.ID, share: signShare}, nil +} + +type SignShare struct { + ID uint16 + share group.Scalar +} + +func (s *SignShare) Verify( + suite Suite, + pubKeySigner *PublicKey, + comSigner *Commitment, + coms []*Commitment, + pubKeyGroup *PublicKey, + msg []byte, +) bool { + if s.ID != comSigner.ID || s.ID == 0 { + return false + } + + aux, err := suite.common(uint(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.share) + 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 uint, msg []byte, pubKey *PublicKey, coms []*Commitment) (aux *commonAux, err error) { + if !sort.SliceIsSorted(coms, func(i, j int) bool { return coms[i].ID < coms[j].ID }) { + return nil, errors.New("frost:commitments must be sorted") + } + + idx := sort.Search(len(coms), func(j int) bool { return uint(coms[j].ID) >= id }) + if !(idx < len(coms) && uint(coms[idx].ID) == 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] = s.g.NewScalar() + peers[i].SetUint64(uint64(coms[i].ID)) + } + + 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 000000000..b02815f9b --- /dev/null +++ b/tss/frost/suites.go @@ -0,0 +1,81 @@ +package frost + +import ( + "crypto" + _ "crypto/sha256" + "crypto/sha512" + "fmt" + + r255 "github.com/bwesterb/go-ristretto" + "github.com/cloudflare/circl/group" +) + +var ( + P256 = Suite{group.P256, suiteP{group.P256, crypto.SHA256, "FROST-P256-SHA256-v5"}} + Ristretto255 = Suite{group.Ristretto255, suiteRis255{"FROST-RISTRETTO255-SHA512-v5"}} +) + +type Suite struct { + g group.Group + hasher interface { + h1(m []byte) group.Scalar + h2(m []byte) group.Scalar + h4(m []byte) group.Scalar + h3(m []byte) []byte + } +} + +func (s Suite) String() string { return s.hasher.(fmt.Stringer).String() } + +const ( + labelRho = "rho" + labelChal = "chal" + labelNonce = "nonce" + labelDigest = "digest" +) + +type suiteP struct { + g group.Group + hash crypto.Hash + context string +} + +func (s suiteP) String() string { return s.context[:len(s.context)-3] } +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) h4(m []byte) group.Scalar { return s.g.HashToScalar(m, []byte(s.context+labelNonce)) } +func (s suiteP) h3(m []byte) []byte { + H := s.hash.New() + _, _ = H.Write([]byte(s.context + labelDigest)) + _, _ = H.Write(m) + return H.Sum(nil) +} + +type suiteRis255 struct { + context string +} + +func (s suiteRis255) String() string { return s.context[:len(s.context)-3] } + +func (s suiteRis255) hashLabeled(m []byte, label string) (sum [64]byte) { + H := sha512.New() + _, _ = H.Write([]byte(s.context + label)) + _, _ = H.Write(m) + copy(sum[:], H.Sum(nil)) + + return +} + +func (s suiteRis255) getScalar(data [64]byte) group.Scalar { + var y r255.Scalar + y.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(m, labelRho)) } +func (s suiteRis255) h2(m []byte) group.Scalar { return s.getScalar(s.hashLabeled(m, labelChal)) } +func (s suiteRis255) h4(m []byte) group.Scalar { return s.getScalar(s.hashLabeled(m, labelNonce)) } +func (s suiteRis255) h3(m []byte) []byte { d := s.hashLabeled(m, labelDigest); return d[:] } diff --git a/tss/frost/testdata/frost_p256_sha256.json b/tss/frost/testdata/frost_p256_sha256.json new file mode 100644 index 000000000..b04fb1755 --- /dev/null +++ b/tss/frost/testdata/frost_p256_sha256.json @@ -0,0 +1,61 @@ +{ + "config": { + "MAX_SIGNERS": "3", + "NUM_SIGNERS": "2", + "MIN_SIGNERS": "2", + "name": "FROST(P-256, SHA-256)", + "group": "P-256", + "hash": "SHA-256" + }, + "inputs": { + "group_secret_key": "8ba9bba2e0fd8c4767154d35a0b7562244a4aaf6f36c8fb8735fa48b301bd8de", + "group_public_key": "023a309ad94e9fe8a7ba45dfc58f38bf091959d3c99cfbd02b4dc00585ec45ab70", + "message": "74657374", + "signers": { + "1": { + "signer_share": "0c9c1a0fe806c184add50bbdcac913dda73e482daf95dcb9f35dbb0d8a9f7731" + }, + "2": { + "signer_share": "8d8e787bef0ff6c2f494ca45f4dad198c6bee01212d6c84067159c52e1863ad5" + }, + "3": { + "signer_share": "0e80d6e8f6192c003b5488ce1eec8f5429587d48cf001541e713b2d53c09d928" + } + } + }, + "round_one_outputs": { + "participants": "1,3", + "signers": { + "1": { + "hiding_nonce": "acad0caab1466c3a075297b7ab651c19cb073964b0d656b9e1483a8dfcbae1d1", + "binding_nonce": "58684e8a3a82d4e4b3ae65bbcee6a5f832a55f84559fdb37b3459aa535ae777a", + "hiding_nonce_commitment": "0352a44580c702ebbe6c865566937dac503c5b8dbed3953c15b8f83e228614b9a1", + "binding_nonce_commitment": "0313ea8be05e83641fe8c5b5564eef02c77b898addf1d8788611910912ff7444cc", + "binding_factor_input": "7a753fed12531fbcd151e1d84702927c39063e780e91c01f02bd11b60d7632bf66abdb7c24a9611565dd71a13492ba16306a602b00b323757aee47f7ebfa7fcb0001", + "binding_factor": "4b6f5a676853c5319975edcbfa2e39a30a3080dc64d0799604a97e5d0e668aab" + }, + "3": { + "hiding_nonce": "3b85caa37d764a7d7e357b38feacfe5aa3c5b517af813e47c84321a68bf0113a", + "binding_nonce": "3413506e1567a91978686c62e2646a07d84489892579a682e2036d128ea33864", + "hiding_nonce_commitment": "0391c2edb8ba65318b372274ebc937da08270e2efe21337c65294783e69a6d3e99", + "binding_nonce_commitment": "03c325c56fc397957875ca50551e8f9d4540b77d02de23821e01e8363fa3e6bd2b", + "binding_factor_input": "7a753fed12531fbcd151e1d84702927c39063e780e91c01f02bd11b60d7632bf66abdb7c24a9611565dd71a13492ba16306a602b00b323757aee47f7ebfa7fcb0003", + "binding_factor": "d2389dd98e4872dd35ecb47e70314b48ee1ece5892de1f08a86adce7d2c79eeb" + } + } + }, + "round_two_outputs": { + "participants": "1,3", + "signers": { + "1": { + "sig_share": "5ac3bca8b2d9c2bc7c036e3f03c364806b2dbed2c884cd57e3381c1747d6432a" + }, + "3": { + "sig_share": "477932b617f3e501fa54a7dc67b4f72353c257bacb0497574ca6fde8f1f40d6c" + } + } + }, + "final_output": { + "sig": "0219f3bddb7c97d4ce69ea226195fb3ed435fc9063bbeb3a45f46a3a384e0229baa23cef5ecacda7be7658161b6b785ba3bef0168d938964af2fdf1a0039ca5096" + } +} \ 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 000000000..efc2a1773 --- /dev/null +++ b/tss/frost/testdata/frost_ristretto255_sha512.json @@ -0,0 +1,61 @@ +{ + "config": { + "MAX_SIGNERS": "3", + "NUM_SIGNERS": "2", + "MIN_SIGNERS": "2", + "name": "FROST(ristretto255, SHA-512)", + "group": "ristretto255", + "hash": "SHA-512" + }, + "inputs": { + "group_secret_key": "1b25a55e463cfd15cf14a5d3acc3d15053f08da49c8afcf3ab265f2ebc4f970b", + "group_public_key": "e2a62f39eede11269e3bd5a7d97554f5ca384f9f6d3dd9c3c0d05083c7254f57", + "message": "74657374", + "signers": { + "1": { + "signer_share": "5c3430d391552f6e60ecdc093ff9f6f4488756aa6cebdbad75a768010b8f830e" + }, + "2": { + "signer_share": "b06fc5eac20b4f6e1b271d9df2343d843e1e1fb03c4cbb673f2872d459ce6f01" + }, + "3": { + "signer_share": "f17e505f0e2581c6acfe54d3846a622834b5e7b50cad9a2109a97ba7a80d5c04" + } + } + }, + "round_one_outputs": { + "participants": "1,3", + "signers": { + "1": { + "hiding_nonce": "054b3d8d592efe078863b7b3557205f3fffa67cf2ef5dd4ea0fd9eb764947502", + "binding_nonce": "7e139b87e1dc61c260c91daba258c37f99b9cb0210866b49e0ebade46f131004", + "hiding_nonce_commitment": "02298f7207986e5e52bf2d98b19c69bde7a33f173462d555ddde0dee8ad51217", + "binding_nonce_commitment": "6291ee7a8cc21e11116da8903af5b59492768f3ee2349827f6e359987c403651", + "binding_factor_input": "678630bf982c566949d7f22d2aefb94f252c664216d332f34e2c8fdcd7045f207f854504d0daa534a5b31dbdf4183be30eb4fdba4f962d8a6b69cf20c273404316a59653b5cd5b094f89f93085078f3fd6fb6247f05d47779b1936d82967e5b48c6a209445de9d14df65c0ce27d490ed22e4c292118783b489759ab330385bad0001", + "binding_factor": "3d8c544930cc456df222182dee9b1675e783631a495522297600a2dd7a839604" + }, + "3": { + "hiding_nonce": "a3bfcd687f52c7cff6a4a9f691bbfeef81b336fe5ef775b89bb2a5fc0647e905", + "binding_nonce": "a47e63cfe377da9edc357830fecc61a58ba453534ea12b7ae2778a2deb56a005", + "hiding_nonce_commitment": "4ade98a01fb42449d9cd992f28a07f5bcae0ef231c18e8ba961718355610b77e", + "binding_nonce_commitment": "90c4f9bda2aafad2677f396677f44578763bf39999fa04ec22080e436e026d2a", + "binding_factor_input": "678630bf982c566949d7f22d2aefb94f252c664216d332f34e2c8fdcd7045f207f854504d0daa534a5b31dbdf4183be30eb4fdba4f962d8a6b69cf20c273404316a59653b5cd5b094f89f93085078f3fd6fb6247f05d47779b1936d82967e5b48c6a209445de9d14df65c0ce27d490ed22e4c292118783b489759ab330385bad0003", + "binding_factor": "e46fbd30404c96368ba771698cf188235f98829ebd82166832ca1b06a0529e0b" + } + } + }, + "round_two_outputs": { + "participants": "1,3", + "signers": { + "1": { + "sig_share": "d74f67b8797567fb7054d541538a2e1b5373e93eaa995d21072c7f368d8d530e" + }, + "3": { + "sig_share": "ebbe5710e183a4f09738a9d727b96c3233f8a0675d1b6feb1fbfbbbcdbd6ab0f" + } + } + }, + "final_output": { + "sig": "02cd31a1d76ab50b2de10fe945824130c6f558c5bf023ea9c2b20091e3df867dd53ac96b4096f99332f086769c49bc38866b8aa607b5cc0c27eb3af36864ff0d" + } +} \ 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 000000000..14854ca1a --- /dev/null +++ b/tss/frost/vectors_test.go @@ -0,0 +1,199 @@ +package frost + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "testing" + + "github.com/cloudflare/circl/group" + "github.com/cloudflare/circl/internal/test" +) + +type vector struct { + Config struct { + MAXSIGNERS uint16 `json:"MAX_SIGNERS,string"` + NUMSIGNERS uint16 `json:"NUM_SIGNERS,string"` + MINSIGNERS uint16 `json:"MIN_SIGNERS,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"` + Signers struct { + Num1 struct { + SignerShare string `json:"signer_share"` + } `json:"1"` + Num2 struct { + SignerShare string `json:"signer_share"` + } `json:"2"` + Num3 struct { + SignerShare string `json:"signer_share"` + } `json:"3"` + } `json:"signers"` + } `json:"inputs"` + RoundOneOutputs struct { + Participants string `json:"participants"` + Signers struct { + Num1 struct { + 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 { + 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:"signers"` + } `json:"round_one_outputs"` + RoundTwoOutputs struct { + Participants string `json:"participants"` + Signers struct { + Num1 struct { + SigShare string `json:"sig_share"` + } `json:"1"` + Num3 struct { + SigShare string `json:"sig_share"` + } `json:"3"` + } `json:"signers"` + } `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, 1, v.Config.NUMSIGNERS, v.Config.MAXSIGNERS, toScalar(t, suite.g, v.Inputs.Signers.Num1.SignerShare, "signer share"), nil} + // p2 := PeerSigner{suite, 2, v.Config.NUMSIGNERS, v.Config.MAXSIGNERS, toScalar(t, suite.g, v.Inputs.Signers.Num2.SignerShare, "signer share"), nil} + p3 := PeerSigner{suite, 3, v.Config.NUMSIGNERS, v.Config.MAXSIGNERS, toScalar(t, suite.g, v.Inputs.Signers.Num3.SignerShare, "signer share"), nil} + + hn1 := toScalar(t, suite.g, v.RoundOneOutputs.Signers.Num1.HidingNonce, "hiding nonce") + bn1 := toScalar(t, suite.g, v.RoundOneOutputs.Signers.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.Signers.Num1.HidingNonceCommitment, "hiding nonce commit")) + compareBytes(t, toBytesElt(t, commit1.binding), fromHex(t, v.RoundOneOutputs.Signers.Num1.BindingNonceCommitment, "binding nonce commit")) + + hn3 := toScalar(t, suite.g, v.RoundOneOutputs.Signers.Num3.HidingNonce, "hiding nonce") + bn3 := toScalar(t, suite.g, v.RoundOneOutputs.Signers.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.Signers.Num3.HidingNonceCommitment, "hiding nonce commit")) + compareBytes(t, toBytesElt(t, commit3.binding), fromHex(t, v.RoundOneOutputs.Signers.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.Signers.Num1.BindingFactor, "binding factor")) + compareBytes(t, toBytesScalar(t, bindingFactors[1].factor), fromHex(t, v.RoundOneOutputs.Signers.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.share), fromHex(t, v.RoundTwoOutputs.Signers.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.share), fromHex(t, v.RoundTwoOutputs.Signers.Num3.SigShare, "sign share")) + + combiner, err := NewCombiner(suite, uint(v.Config.MINSIGNERS-1), uint(v.Config.MAXSIGNERS)) + 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, _ := ioutil.ReadAll(jsonFile) + + 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-07 + // Test vectors at https://github.com/cfrg/draft-irtf-cfrg-frost + // Version supported: v07 + 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 000000000..f3e650b43 --- /dev/null +++ b/tss/tss.go @@ -0,0 +1,2 @@ +// Package tss provides threshold signature schemes. +package tss