diff --git a/tss/frost/combiner.go b/tss/frost/combiner.go new file mode 100644 index 000000000..a5059751a --- /dev/null +++ b/tss/frost/combiner.go @@ -0,0 +1,83 @@ +package frost + +import "fmt" + +type Coordinator struct { + Suite + threshold uint + maxSigners uint +} + +func NewCoordinator(s Suite, threshold, maxSigners uint) (*Coordinator, error) { + if threshold > maxSigners { + return nil, fmt.Errorf("frost: invalid parameters") + } + + return &Coordinator{Suite: s, threshold: threshold, maxSigners: maxSigners}, nil +} + +func (c Coordinator) CheckSignShares( + msg []byte, + groupPublicKey PublicKey, + signShares []SignShare, + coms []Commitment, + pubKeySigners []PublicKey, +) 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(msg, groupPublicKey, pubKeySigners[i], coms[i], coms) { + return false + } + } + + return true +} + +func (c Coordinator) Aggregate( + msg []byte, + groupPublicKey PublicKey, + signShares []SignShare, + coms []Commitment, +) ([]byte, error) { + if l := len(coms); l <= int(c.threshold) { + return nil, fmt.Errorf("frost: only %v shares of %v required", l, c.threshold) + } + + p := c.Suite.getParams() + bindingFactors, err := getBindingFactors(p, msg, groupPublicKey, coms) + if err != nil { + return nil, err + } + + g := p.group() + groupCom, err := getGroupCommitment(g, coms, bindingFactors) + if err != nil { + return nil, err + } + + gcEnc, err := groupCom.MarshalBinaryCompress() + if err != nil { + return nil, err + } + + z := 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 000000000..b899f7849 --- /dev/null +++ b/tss/frost/commit.go @@ -0,0 +1,128 @@ +package frost + +import ( + "fmt" + "sort" + + "github.com/cloudflare/circl/group" +) + +type Nonce struct { + id group.Scalar + hiding group.Scalar + binding group.Scalar +} + +func nonceGenerate(p params, randomBytes, secretEnc []byte) group.Scalar { + return p.h3(append(append([]byte{}, randomBytes...), secretEnc...)) +} + +type Commitment struct { + id group.Scalar + hiding group.Element + 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) (out []byte, err error) { + sort.SliceStable(coms, func(i, j int) bool { + return coms[i].id.(fmt.Stringer).String() < coms[j].id.(fmt.Stringer).String() + }) + + 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 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, fmt.Errorf("frost: id not found") +} + +func getBindingFactors(p params, msg []byte, groupPublicKey PublicKey, coms []Commitment) ([]bindingFactor, error) { + groupPublicKeyEnc, err := groupPublicKey.key.MarshalBinaryCompress() + if err != nil { + return nil, err + } + + msgHash := p.h4(msg) + encodeComs, err := encodeCommitments(coms) + if err != nil { + return nil, err + } + encodeComsHash := p.h5(encodeComs) + rhoInputPrefix := append(append(groupPublicKeyEnc, 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 := p.h1(rhoInput) + bindingFactors[i] = bindingFactor{ID: coms[i].id, factor: bf} + } + + return bindingFactors, nil +} + +func getGroupCommitment(g group.Group, coms []Commitment, bindingFactors []bindingFactor) (group.Element, error) { + gc := g.NewElement() + tmp := g.NewElement() + for i := range coms { + bf, err := 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 getChallenge(p params, groupCom group.Element, msg []byte, pubKey PublicKey) (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 p.h2(chInput), nil +} diff --git a/tss/frost/frost.go b/tss/frost/frost.go new file mode 100644 index 000000000..1c701225b --- /dev/null +++ b/tss/frost/frost.go @@ -0,0 +1,98 @@ +// Package frost provides the FROST threshold signature scheme for Schnorr signatures. +// +// FROST paper: https://eprint.iacr.org/2020/852 +// RFC 9519: https://www.rfc-editor.org/rfc/rfc9591 +package frost + +import ( + "io" + + "github.com/cloudflare/circl/group" + "github.com/cloudflare/circl/secretsharing" +) + +type PrivateKey struct { + Suite + key group.Scalar + publicKey *PublicKey +} + +type PublicKey struct { + Suite + key group.Element +} + +func GenerateKey(s Suite, rnd io.Reader) PrivateKey { + g := s.getParams().group() + return PrivateKey{s, g.RandomNonZeroScalar(rnd), nil} +} + +func (k *PrivateKey) PublicKey() PublicKey { + if k.publicKey == nil { + g := k.Suite.getParams().group() + k.publicKey = &PublicKey{k.Suite, g.NewElement().MulGen(k.key)} + } + + return *k.publicKey +} + +func (k *PrivateKey) Split(rnd io.Reader, threshold, maxSigners uint) ( + peers []PeerSigner, groupPublicKey PublicKey, comm secretsharing.SecretCommitment, +) { + ss := secretsharing.New(rnd, threshold, k.key) + shares := ss.Share(maxSigners) + comm = ss.CommitSecret() + groupPublicKey = PublicKey{k.Suite, comm[0]} + + peers = make([]PeerSigner, len(shares)) + for i := range shares { + peers[i] = PeerSigner{ + Suite: k.Suite, + threshold: uint16(threshold), + maxSigners: uint16(maxSigners), + keyShare: shares[i], + groupPublicKey: groupPublicKey, + myPublicKey: nil, + } + } + + return peers, groupPublicKey, comm +} + +func Verify(msg []byte, pubKey PublicKey, signature []byte) bool { + p := pubKey.Suite.getParams() + g := p.group() + params := g.Params() + Ne, Ns := params.CompressedElementLength, params.ScalarLength + if len(signature) < int(Ne+Ns) { + return false + } + + REnc := signature[:Ne] + R := g.NewElement() + err := R.UnmarshalBinary(REnc) + if err != nil { + return false + } + + zEnc := signature[Ne : Ne+Ns] + z := 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 := p.h2(chInput) + + l := g.NewElement().MulGen(z) + r := 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..fcb21976e --- /dev/null +++ b/tss/frost/frost_test.go @@ -0,0 +1,154 @@ +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) + peers, groupPublicKey, keyShareCommits := privKey.Split(rand.Reader, t, n) + + // 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] { + nonce, commit, err := peers[i].Commit(rand.Reader) + test.CheckNoErr(tt, err, "failed to commit") + pkSigners[i] = peers[i].PublicKey() + nonces[i] = *nonce + commits[i] = *commit + } + + // round 2 + msg := []byte("it's cold here") + signShares := make([]frost.SignShare, k) + for i := range peers[:k] { + sigShare, err := peers[i].Sign(msg, groupPublicKey, nonces[i], commits) + test.CheckNoErr(tt, err, "failed to create a sign share") + signShares[i] = *sigShare + } + + // Coordinator + coordinator, err := frost.NewCoordinator(suite, t, n) + test.CheckNoErr(tt, err, "failed to create combiner") + + valid := coordinator.CheckSignShares(msg, groupPublicKey, signShares, commits, pkSigners) + if k > t { + test.CheckOk(valid == true, "invalid sign shares", tt) + } else { + test.CheckOk(valid == false, "must be invalid sign shares", tt) + } + + signature, err := coordinator.Aggregate(msg, groupPublicKey, signShares, commits) + if k > t { + test.CheckNoErr(tt, err, "failed to produce signature") + // anyone can verify + valid := frost.Verify(msg, groupPublicKey, 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, groupPublicKey, keyShareCommits := privKey.Split(rand.Reader, t, n) + + 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 { + nonce, commit, err := peers[i].Commit(rand.Reader) + test.CheckNoErr(b, err, "failed to commit") + pkSigners[i] = peers[i].PublicKey() + nonces[i] = *nonce + commits[i] = *commit + } + + signShares := make([]frost.SignShare, len(peers)) + for i := range peers { + sigShare, err := peers[i].Sign(msg, groupPublicKey, nonces[i], commits) + test.CheckNoErr(b, err, "failed to create a sign share") + signShares[i] = *sigShare + } + coordinator, err := frost.NewCoordinator(suite, t, n) + test.CheckNoErr(b, err, "failed to create combiner") + signature, err := coordinator.Aggregate(msg, groupPublicKey, signShares, commits) + test.CheckNoErr(b, err, "failed to aggregate") + valid := frost.Verify(msg, groupPublicKey, signature) + test.CheckOk(valid, "failed to verify", b) + + b.Run("SplitKey", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _, _ = privKey.Split(rand.Reader, t, n) + } + }) + + 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, groupPublicKey, nonces[0], commits) + } + }) + + b.Run("CheckSignShares", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = coordinator.CheckSignShares(msg, groupPublicKey, signShares, commits, pkSigners) + } + }) + + b.Run("Aggregate", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = coordinator.Aggregate(msg, groupPublicKey, signShares, commits) + } + }) + + b.Run("Verify", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = frost.Verify(msg, groupPublicKey, signature) + } + }) +} diff --git a/tss/frost/peer.go b/tss/frost/peer.go new file mode 100644 index 000000000..1a18a17bf --- /dev/null +++ b/tss/frost/peer.go @@ -0,0 +1,185 @@ +package frost + +import ( + "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 + myPublicKey *PublicKey + groupPublicKey PublicKey +} + +func (p *PeerSigner) Commit(rnd io.Reader) (*Nonce, *Commitment, error) { + var hidingNonceRandomness [32]byte + _, err := io.ReadFull(rnd, hidingNonceRandomness[:]) + if err != nil { + return nil, nil, err + } + + var bindingNonceRandomness [32]byte + _, err = io.ReadFull(rnd, bindingNonceRandomness[:]) + if err != nil { + return nil, nil, err + } + + return p.commitWithRandomness(hidingNonceRandomness[:], bindingNonceRandomness[:]) +} + +func (p *PeerSigner) commitWithRandomness(hidingNonceRnd, bindingNonceRnd []byte) (*Nonce, *Commitment, error) { + secretEnc, err := p.keyShare.Value.MarshalBinary() + if err != nil { + return nil, nil, err + } + + pp := p.Suite.getParams() + g := pp.group() + hidingNonce := nonceGenerate(pp, hidingNonceRnd, secretEnc) + hidingNonceCom := g.NewElement().MulGen(hidingNonce) + + bindingNonce := nonceGenerate(pp, bindingNonceRnd, secretEnc) + bindingNonceCom := g.NewElement().MulGen(bindingNonce) + + return &Nonce{p.keyShare.ID, hidingNonce, bindingNonce}, + &Commitment{p.keyShare.ID, hidingNonceCom, bindingNonceCom}, + nil +} + +func (p *PeerSigner) CheckKeyShare(c secretsharing.SecretCommitment) bool { + return secretsharing.Verify(uint(p.threshold), p.keyShare, c) +} + +func (p *PeerSigner) PublicKey() PublicKey { + if p.myPublicKey == nil { + g := p.Suite.getParams().group() + p.myPublicKey = &PublicKey{p.Suite, g.NewElement().MulGen(p.keyShare.Value)} + } + + return *p.myPublicKey +} + +func (p *PeerSigner) Sign(msg []byte, pubKey PublicKey, nonce Nonce, coms []Commitment) (*SignShare, error) { + if !p.keyShare.ID.IsEqual(nonce.id) { + return nil, fmt.Errorf("frost: bad id") + } + + pp := p.Suite.getParams() + aux, err := common(pp, p.keyShare.ID, msg, pubKey, coms) + if err != nil { + return nil, err + } + + g := pp.group() + tmp := g.NewScalar().Mul(nonce.binding, aux.bindingFactor) + signShare := g.NewScalar().Add(nonce.hiding, tmp) + tmp.Mul(aux.lambdaID, p.keyShare.Value) + tmp.Mul(tmp, aux.challenge) + signShare.Add(signShare, tmp) + + return &SignShare{ + Suite: p.Suite, + s: secretsharing.Share{ID: p.keyShare.ID, Value: signShare}, + }, nil +} + +type SignShare struct { + Suite + s secretsharing.Share +} + +func (s SignShare) Verify( + msg []byte, + groupPublicKey PublicKey, + pubKeySigner PublicKey, + comSigner Commitment, + coms []Commitment, +) bool { + if s.s.ID != comSigner.id || s.s.ID.IsZero() { + return false + } + + pp := s.Suite.getParams() + aux, err := common(pp, s.s.ID, msg, groupPublicKey, coms) + if err != nil { + return false + } + + g := pp.group() + comShare := g.NewElement().Mul(coms[aux.idx].binding, aux.bindingFactor) + comShare.Add(comShare, coms[aux.idx].hiding) + + l := g.NewElement().MulGen(s.s.Value) + r := g.NewElement().Mul(pubKeySigner.key, 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 common(p params, id group.Scalar, msg []byte, groupPublicKey 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, fmt.Errorf("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, fmt.Errorf("frost: commitment not present") + } + + bindingFactors, err := getBindingFactors(p, msg, groupPublicKey, coms) + if err != nil { + return nil, err + } + + bindingFactor, err := getBindingFactorFromID(bindingFactors, id) + if err != nil { + return nil, err + } + + g := p.group() + groupCom, err := getGroupCommitment(g, coms, bindingFactors) + if err != nil { + return nil, err + } + + challenge, err := getChallenge(p, groupCom, msg, groupPublicKey) + if err != nil { + return nil, err + } + + peers := make([]group.Scalar, len(coms)) + for i := range coms { + peers[i] = coms[i].id.Copy() + } + + zero := 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..54b4bb6cc --- /dev/null +++ b/tss/frost/suites.go @@ -0,0 +1,100 @@ +package frost + +import ( + "crypto" + _ "crypto/sha256" // added to link library. + _ "crypto/sha512" // added to link library. + + r255 "github.com/bwesterb/go-ristretto" + "github.com/cloudflare/circl/group" +) + +type Suite uint8 + +const ( + Ristretto255 Suite = iota + P256 +) + +func (s Suite) String() string { + switch s { + case Ristretto255: + return paramsRis.String() + case P256: + return paramsP256.String() + default: + return "frost: undefined suite" + } +} + +var ( + paramsRis = &suiteRis255{suiteCommon{group.Ristretto255, crypto.SHA512, "FROST-RISTRETTO255-SHA512-v1"}} + paramsP256 = &suiteP{suiteCommon{group.P256, crypto.SHA256, "FROST-P256-SHA256-v1"}} +) + +func (s Suite) getParams() params { + switch s { + case Ristretto255: + return paramsRis + case P256: + return paramsP256 + default: + panic("frost: undefined suite") + } +} + +type params interface { + group() group.Group + h1(m []byte) group.Scalar + h2(m []byte) group.Scalar + h3(m []byte) group.Scalar + h4(m []byte) []byte + h5(m []byte) []byte +} + +const ( + labelRho = "rho" + labelChal = "chal" + labelNonce = "nonce" + labelMsg = "msg" + labelCom = "com" +) + +type suiteCommon struct { + g group.Group + hash crypto.Hash + context string +} + +func (s suiteCommon) String() string { return s.context[:len(s.context)-3] } +func (s suiteCommon) group() group.Group { return s.g } +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{ 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/rfc9591_frost_p256_sha256.json b/tss/frost/testdata/rfc9591_frost_p256_sha256.json new file mode 100644 index 000000000..fdad9410d --- /dev/null +++ b/tss/frost/testdata/rfc9591_frost_p256_sha256.json @@ -0,0 +1,77 @@ +{ + "config": { + "MAX_PARTICIPANTS": "3", + "NUM_PARTICIPANTS": "2", + "MIN_PARTICIPANTS": "2", + "name": "FROST(P-256, SHA-256)", + "group": "P-256", + "hash": "SHA-256" + }, + "inputs": { + "participant_list": [ + 1, + 3 + ], + "group_secret_key": "8ba9bba2e0fd8c4767154d35a0b7562244a4aaf6f36c8fb8735fa48b301bd8de", + "group_public_key": "023a309ad94e9fe8a7ba45dfc58f38bf091959d3c99cfbd02b4dc00585ec45ab70", + "message": "74657374", + "share_polynomial_coefficients": [ + "80f25e6c0709353e46bfbe882a11bdbb1f8097e46340eb8673b7e14556e6c3a4" + ], + "participant_shares": [ + { + "identifier": 1, + "participant_share": "0c9c1a0fe806c184add50bbdcac913dda73e482daf95dcb9f35dbb0d8a9f7731" + }, + { + "identifier": 2, + "participant_share": "8d8e787bef0ff6c2f494ca45f4dad198c6bee01212d6c84067159c52e1863ad5" + }, + { + "identifier": 3, + "participant_share": "0e80d6e8f6192c003b5488ce1eec8f5429587d48cf001541e713b2d53c09d928" + } + ] + }, + "round_one_outputs": { + "outputs": [ + { + "identifier": 1, + "hiding_nonce_randomness": "ec4c891c85fee802a9d757a67d1252e7f4e5efb8a538991ac18fbd0e06fb6fd3", + "binding_nonce_randomness": "9334e29d09061223f69a09421715a347e4e6deba77444c8f42b0c833f80f4ef9", + "hiding_nonce": "9f0542a5ba879a58f255c09f06da7102ef6a2dec6279700c656d58394d8facd4", + "binding_nonce": "6513dfe7429aa2fc972c69bb495b27118c45bbc6e654bb9dc9be55385b55c0d7", + "hiding_nonce_commitment": "0213b3e6298bf8ad46fd5e9389519a8665d63d98f4ec6a1fcca434e809d2d8070e", + "binding_nonce_commitment": "02188ff1390bf69374d7b272e454b1878ef10a6b6ea3ff36f114b300b4dbd5233b", + "binding_factor_input": "023a309ad94e9fe8a7ba45dfc58f38bf091959d3c99cfbd02b4dc00585ec45ab70825371853e974bc30ac5b947b216d70461919666584c70c51f9f56f117736c5d178dd0b521ad9c1abe98048419cbdec81504c85e12eb40e3bcb6ec73d3fc4afd0000000000000000000000000000000000000000000000000000000000000001", + "binding_factor": "7925f0d4693f204e6e59233e92227c7124664a99739d2c06b81cf64ddf90559e" + }, + { + "identifier": 3, + "hiding_nonce_randomness": "c0451c5a0a5480d6c1f860e5db7d655233dca2669fd90ff048454b8ce983367b", + "binding_nonce_randomness": "2ba5f7793ae700e40e78937a82f407dd35e847e33d1e607b5c7eb6ed2a8ed799", + "hiding_nonce": "f73444a8972bcda9e506bbca3d2b1c083c10facdf4bb5d47fef7c2dc1d9f2a0d", + "binding_nonce": "44c6a29075d6e7e4f8b97796205f9e22062e7835141470afe9417fd317c1c303", + "hiding_nonce_commitment": "033ac9a5fe4a8b57316ba1c34e8a6de453033b750e8984924a984eb67a11e73a3f", + "binding_nonce_commitment": "03a7a2480ee16199262e648aea3acab628a53e9b8c1945078f2ddfbdc98b7df369", + "binding_factor_input": "023a309ad94e9fe8a7ba45dfc58f38bf091959d3c99cfbd02b4dc00585ec45ab70825371853e974bc30ac5b947b216d70461919666584c70c51f9f56f117736c5d178dd0b521ad9c1abe98048419cbdec81504c85e12eb40e3bcb6ec73d3fc4afd0000000000000000000000000000000000000000000000000000000000000003", + "binding_factor": "e10d24a8a403723bcb6f9bb4c537f316593683b472f7a89f166630dde11822c4" + } + ] + }, + "round_two_outputs": { + "outputs": [ + { + "identifier": 1, + "sig_share": "400308eaed7a2ddee02a265abe6a1cfe04d946ee8720768899619cfabe7a3aeb" + }, + { + "identifier": 3, + "sig_share": "561da3c179edbb0502d941bb3e3ace3c37d122aaa46fb54499f15f3a3331de44" + } + ] + }, + "final_output": { + "sig": "026d8d434874f87bdb7bc0dfd239b2c00639044f9dcb195e9a04426f70bfa4b70d9620acac6767e8e3e3036815fca4eb3a3caa69992b902bcd3352fc34f1ac192f" + } +} \ No newline at end of file diff --git a/tss/frost/testdata/rfc9591_frost_ristretto255_sha512.json b/tss/frost/testdata/rfc9591_frost_ristretto255_sha512.json new file mode 100644 index 000000000..5e39a553d --- /dev/null +++ b/tss/frost/testdata/rfc9591_frost_ristretto255_sha512.json @@ -0,0 +1,77 @@ +{ + "config": { + "MAX_PARTICIPANTS": "3", + "NUM_PARTICIPANTS": "2", + "MIN_PARTICIPANTS": "2", + "name": "FROST(ristretto255, SHA-512)", + "group": "ristretto255", + "hash": "SHA-512" + }, + "inputs": { + "participant_list": [ + 1, + 3 + ], + "group_secret_key": "1b25a55e463cfd15cf14a5d3acc3d15053f08da49c8afcf3ab265f2ebc4f970b", + "group_public_key": "e2a62f39eede11269e3bd5a7d97554f5ca384f9f6d3dd9c3c0d05083c7254f57", + "message": "74657374", + "share_polynomial_coefficients": [ + "410f8b744b19325891d73736923525a4f596c805d060dfb9c98009d34e3fec02" + ], + "participant_shares": [ + { + "identifier": 1, + "participant_share": "5c3430d391552f6e60ecdc093ff9f6f4488756aa6cebdbad75a768010b8f830e" + }, + { + "identifier": 2, + "participant_share": "b06fc5eac20b4f6e1b271d9df2343d843e1e1fb03c4cbb673f2872d459ce6f01" + }, + { + "identifier": 3, + "participant_share": "f17e505f0e2581c6acfe54d3846a622834b5e7b50cad9a2109a97ba7a80d5c04" + } + ] + }, + "round_one_outputs": { + "outputs": [ + { + "identifier": 1, + "hiding_nonce_randomness": "f595a133b4d95c6e1f79887220c8b275ce6277e7f68a6640e1e7140f9be2fb5c", + "binding_nonce_randomness": "34dd1001360e3513cb37bebfabe7be4a32c5bb91ba19fbd4360d039111f0fbdc", + "hiding_nonce": "214f2cabb86ed71427ea7ad4283b0fae26b6746c801ce824b83ceb2b99278c03", + "binding_nonce": "c9b8f5e16770d15603f744f8694c44e335e8faef00dad182b8d7a34a62552f0c", + "hiding_nonce_commitment": "965def4d0958398391fc06d8c2d72932608b1e6255226de4fb8d972dac15fd57", + "binding_nonce_commitment": "ec5170920660820007ae9e1d363936659ef622f99879898db86e5bf1d5bf2a14", + "binding_factor_input": "e2a62f39eede11269e3bd5a7d97554f5ca384f9f6d3dd9c3c0d05083c7254f572889dde2854e26377a16caf77dfee5f6be8fe5b4c80318da84698a4161021b033911db5ef8205362701bc9ecd983027814abee94f46d094943a2f4b79a6e4d4603e52c435d8344554942a0a472d8ad84320585b8da3ae5b9ce31cd1903f795c1af66de22af1a45f652cd05ee446b1b4091aaccc91e2471cd18a85a659cecd11f0100000000000000000000000000000000000000000000000000000000000000", + "binding_factor": "8967fd70fa06a58e5912603317fa94c77626395a695a0e4e4efc4476662eba0c" + }, + { + "identifier": 3, + "hiding_nonce_randomness": "daa0cf42a32617786d390e0c7edfbf2efbd428037069357b5173ae61d6dd5d5e", + "binding_nonce_randomness": "b4387e72b2e4108ce4168931cc2c7fcce5f345a5297368952c18b5fc8473f050", + "hiding_nonce": "3f7927872b0f9051dd98dd73eb2b91494173bbe0feb65a3e7e58d3e2318fa40f", + "binding_nonce": "ffd79445fb8030f0a3ddd3861aa4b42b618759282bfe24f1f9304c7009728305", + "hiding_nonce_commitment": "480e06e3de182bf83489c45d7441879932fd7b434a26af41455756264fbd5d6e", + "binding_nonce_commitment": "3064746dfd3c1862ef58fc68c706da287dd925066865ceacc816b3a28c7b363b", + "binding_factor_input": "e2a62f39eede11269e3bd5a7d97554f5ca384f9f6d3dd9c3c0d05083c7254f572889dde2854e26377a16caf77dfee5f6be8fe5b4c80318da84698a4161021b033911db5ef8205362701bc9ecd983027814abee94f46d094943a2f4b79a6e4d4603e52c435d8344554942a0a472d8ad84320585b8da3ae5b9ce31cd1903f795c1af66de22af1a45f652cd05ee446b1b4091aaccc91e2471cd18a85a659cecd11f0300000000000000000000000000000000000000000000000000000000000000", + "binding_factor": "f2c1bb7c33a10511158c2f1766a4a5fadf9f86f2a92692ed333128277cc31006" + } + ] + }, + "round_two_outputs": { + "outputs": [ + { + "identifier": 1, + "sig_share": "9285f875923ce7e0c491a592e9ea1865ec1b823ead4854b48c8a46287749ee09" + }, + { + "identifier": 3, + "sig_share": "7cb211fe0e3d59d25db6e36b3fb32344794139602a7b24f1ae0dc4e26ad7b908" + } + ] + }, + "final_output": { + "sig": "fc45655fbc66bbffad654ea4ce5fdae253a49a64ace25d9adb62010dd9fb25552164141787162e5b4cab915b4aa45d94655dbb9ed7c378a53b980a0be220a802" + } +} \ 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..ff3648b1a --- /dev/null +++ b/tss/frost/vectors_test.go @@ -0,0 +1,213 @@ +package frost + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "testing" + + "github.com/cloudflare/circl/group" + "github.com/cloudflare/circl/internal/test" + "github.com/cloudflare/circl/secretsharing" +) + +type vector struct { + Config struct { + MAXSIGNERS int `json:"MAX_PARTICIPANTS,string"` + NUMSIGNERS int `json:"NUM_PARTICIPANTS,string"` + MINSIGNERS int `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"` + PolyCoeffs []string `json:"share_polynomial_coefficients"` + Signers []int `json:"participant_list"` + Shares []struct { + ID int `json:"identifier"` + SignerShare string `json:"participant_share"` + } `json:"participant_shares"` + } `json:"inputs"` + RoundOneOutputs struct { + Outputs []struct { + ID int `json:"identifier"` + HidingNonceRnd string `json:"hiding_nonce_randomness"` + BindingNonceRnd 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:"outputs"` + } `json:"round_one_outputs"` + RoundTwoOutputs struct { + Outputs []struct { + ID int `json:"identifier"` + SigShare string `json:"sig_share"` + } `json:"outputs"` + } `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, fmt.Sprintf("%x", got), fmt.Sprintf("%x", want)) + } +} + +func (v *vector) test(t *testing.T, s Suite) { + Threshold := v.Config.MINSIGNERS - 1 + NumPeers := v.Config.NUMSIGNERS + MaxPeers := v.Config.MAXSIGNERS + + test.CheckOk(MaxPeers == len(v.Inputs.Shares), "bad number of shares", t) + test.CheckOk(NumPeers == len(v.Inputs.Signers), "bad number of signers", t) + test.CheckOk(NumPeers == len(v.RoundOneOutputs.Outputs), "bad number of outputs round one", t) + test.CheckOk(NumPeers == len(v.RoundTwoOutputs.Outputs), "bad number of outputs round two", t) + + params := s.getParams() + g := params.group() + privKey := PrivateKey{s, toScalar(t, g, v.Inputs.GroupSecretKey, "bad private key"), nil} + groupPublicKey := privKey.PublicKey() + compareBytes(t, toBytesElt(t, groupPublicKey.key), fromHex(t, v.Inputs.GroupPublicKey, "bad public key")) + + peers := make(map[int]PeerSigner) + for _, inputs := range v.Inputs.Shares { + keyShare := secretsharing.Share{ + ID: g.NewScalar().SetUint64(uint64(inputs.ID)), + Value: toScalar(t, g, inputs.SignerShare, "peer share"), + } + peers[inputs.ID] = PeerSigner{ + Suite: s, + threshold: uint16(Threshold), + maxSigners: uint16(MaxPeers), + keyShare: keyShare, + groupPublicKey: groupPublicKey, + myPublicKey: nil, + } + } + + var commitList []Commitment + var pkSigners []PublicKey + nonces := make(map[int]Nonce) + + for _, roundOne := range v.RoundOneOutputs.Outputs { + peer := peers[roundOne.ID] + hnr := fromHex(t, roundOne.HidingNonceRnd, "hiding nonce rand") + bnr := fromHex(t, roundOne.BindingNonceRnd, "binding nonce rand") + + nonce, commit, err := peer.commitWithRandomness(hnr, bnr) + test.CheckNoErr(t, err, "failed to commit") + + compareBytes(t, toBytesScalar(t, nonce.hiding), fromHex(t, roundOne.HidingNonce, "hiding nonce")) + compareBytes(t, toBytesScalar(t, nonce.binding), fromHex(t, roundOne.BindingNonce, "binding nonce")) + compareBytes(t, toBytesElt(t, commit.hiding), fromHex(t, roundOne.HidingNonceCommitment, "hiding nonce commit")) + compareBytes(t, toBytesElt(t, commit.binding), fromHex(t, roundOne.BindingNonceCommitment, "binding nonce commit")) + + nonces[roundOne.ID] = *nonce + commitList = append(commitList, *commit) + pkSigners = append(pkSigners, peer.PublicKey()) + } + + msg := fromHex(t, v.Inputs.Message, "bad msg") + bindingFactors, err := getBindingFactors(params, msg, groupPublicKey, commitList) + test.CheckNoErr(t, err, "failed to get binding factors") + + for i := range bindingFactors { + compareBytes(t, toBytesScalar(t, bindingFactors[i].factor), fromHex(t, v.RoundOneOutputs.Outputs[i].BindingFactor, "binding factor")) + } + + var signShareList []SignShare + for _, roundTwo := range v.RoundTwoOutputs.Outputs { + peer := peers[roundTwo.ID] + signShare, errr := peer.Sign(msg, groupPublicKey, nonces[roundTwo.ID], commitList) + test.CheckNoErr(t, errr, "failed to sign share") + + compareBytes(t, toBytesScalar(t, signShare.s.ID), toBytesScalar(t, g.NewScalar().SetUint64(uint64(roundTwo.ID)))) + compareBytes(t, toBytesScalar(t, signShare.s.Value), fromHex(t, roundTwo.SigShare, "sign share")) + + signShareList = append(signShareList, *signShare) + } + + coordinator, err := NewCoordinator(s, uint(Threshold), uint(MaxPeers)) + test.CheckNoErr(t, err, "failed to create combiner") + + ok := coordinator.CheckSignShares(msg, groupPublicKey, signShareList, commitList, pkSigners) + test.CheckOk(ok == true, "invalid signature shares", t) + + signature, err := coordinator.Aggregate(msg, groupPublicKey, signShareList, commitList) + test.CheckNoErr(t, err, "failed to create signature") + compareBytes(t, signature, fromHex(t, v.FinalOutput.Sig, "signature")) + + valid := Verify(msg, groupPublicKey, signature) + test.CheckOk(valid == true, "invalid signature", t) +} + +func readFile(t *testing.T, fileName string) *vector { + t.Helper() + input, err := os.ReadFile(fileName) + if err != nil { + t.Fatalf("File %v can not be opened. 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) { + // RFC 9519: https://www.rfc-editor.org/rfc/rfc9591 + // Test vectors at https://github.com/cfrg/draft-irtf-cfrg-frost + suite, vector := P256, readFile(t, "testdata/rfc9591_frost_p256_sha256.json") + t.Run(fmt.Sprintf("%v", suite), func(tt *testing.T) { vector.test(tt, suite) }) + + suite, vector = Ristretto255, readFile(t, "testdata/rfc9591_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