Skip to content

Commit 5c22ce5

Browse files
committed
FROST: Threshold Signature Scheme
Base implementation by @armfazh taken from this PR: cloudflare#349
1 parent d18cc37 commit 5c22ce5

File tree

14 files changed

+1105
-22
lines changed

14 files changed

+1105
-22
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,6 @@ circl_plugin.so: circl.go
6868

6969
circl.go:
7070
go run .etc/all_imports.go -out $@
71+
72+
updates:
73+
@GOWORK=off go list -u -f '{{if (and (not (or .Main .Indirect)) .Update)}}{{.Path}}: [{{.Version}} -> {{.Update.Version}}]{{end}}' -mod=mod -m all 2> /dev/null

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ go 1.19
44

55
require (
66
github.com/bwesterb/go-ristretto v1.2.3
7-
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a
8-
golang.org/x/sys v0.3.0
7+
golang.org/x/crypto v0.9.0
8+
golang.org/x/sys v0.9.0
99
)

go.sum

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw=
22
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
3-
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a h1:diz9pEYuTIuLMJLs3rGDkeaTsNyRs6duYdFyPAxzE/U=
4-
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
5-
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
6-
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
3+
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
4+
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
5+
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
6+
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
7+
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
8+
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
9+
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=

tss/frost/combiner.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package frost
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
)
7+
8+
type Combiner struct {
9+
Suite
10+
threshold uint
11+
maxSigners uint
12+
}
13+
14+
func NewCombiner(s Suite, threshold, maxSigners uint) (*Combiner, error) {
15+
if threshold > maxSigners {
16+
return nil, errors.New("frost: invalid parameters")
17+
}
18+
19+
return &Combiner{Suite: s, threshold: threshold, maxSigners: maxSigners}, nil
20+
}
21+
22+
func (c Combiner) CheckSignShares(
23+
signShares []*SignShare,
24+
pubKeySigners []*PublicKey,
25+
coms []*Commitment,
26+
pubKeyGroup *PublicKey,
27+
msg []byte,
28+
) bool {
29+
if l := len(signShares); !(int(c.threshold) < l && l <= int(c.maxSigners)) {
30+
return false
31+
}
32+
if l := len(pubKeySigners); !(int(c.threshold) < l && l <= int(c.maxSigners)) {
33+
return false
34+
}
35+
if l := len(coms); !(int(c.threshold) < l && l <= int(c.maxSigners)) {
36+
return false
37+
}
38+
39+
for i := range signShares {
40+
if !signShares[i].Verify(c.Suite, pubKeySigners[i], coms[i], coms, pubKeyGroup, msg) {
41+
return false
42+
}
43+
}
44+
45+
return true
46+
}
47+
48+
func (c Combiner) Sign(msg []byte, coms []*Commitment, signShares []*SignShare) ([]byte, error) {
49+
if l := len(coms); l <= int(c.threshold) {
50+
return nil, fmt.Errorf("frost: only %v shares of %v required", l, c.threshold)
51+
}
52+
53+
bindingFactors, err := c.Suite.getBindingFactors(coms, msg)
54+
if err != nil {
55+
return nil, err
56+
}
57+
58+
groupCom, err := c.Suite.getGroupCommitment(coms, bindingFactors)
59+
if err != nil {
60+
return nil, err
61+
}
62+
63+
gcEnc, err := groupCom.MarshalBinaryCompress()
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
z := c.Suite.g.NewScalar()
69+
for i := range signShares {
70+
z.Add(z, signShares[i].s.Value)
71+
}
72+
73+
zEnc, err := z.MarshalBinary()
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
return append(append([]byte{}, gcEnc...), zEnc...), nil
79+
}

tss/frost/commit.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package frost
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io"
7+
"sort"
8+
9+
"go.bryk.io/circl/group"
10+
)
11+
12+
type Nonce struct {
13+
ID group.Scalar
14+
hiding, binding group.Scalar
15+
}
16+
17+
func (s Suite) nonceGenerate(rnd io.Reader, secret group.Scalar) (group.Scalar, error) {
18+
randomBytes := make([]byte, 32)
19+
_, err := io.ReadFull(rnd, randomBytes)
20+
if err != nil {
21+
return nil, err
22+
}
23+
secretEnc, err := secret.MarshalBinary()
24+
if err != nil {
25+
return nil, err
26+
}
27+
28+
return s.hasher.h3(append(randomBytes, secretEnc...)), nil
29+
}
30+
31+
type Commitment struct {
32+
ID group.Scalar
33+
hiding, binding group.Element
34+
}
35+
36+
func (c Commitment) MarshalBinary() ([]byte, error) {
37+
id, err := c.ID.MarshalBinary()
38+
if err != nil {
39+
return nil, err
40+
}
41+
h, err := c.hiding.MarshalBinaryCompress()
42+
if err != nil {
43+
return nil, err
44+
}
45+
b, err := c.binding.MarshalBinaryCompress()
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
return append(append(id, h...), b...), nil
51+
}
52+
53+
func encodeCommitments(coms []*Commitment) ([]byte, error) {
54+
sort.SliceStable(coms, func(i, j int) bool {
55+
return coms[i].ID.(fmt.Stringer).String() < coms[j].ID.(fmt.Stringer).String()
56+
})
57+
58+
var out []byte
59+
for i := range coms {
60+
cEnc, err := coms[i].MarshalBinary()
61+
if err != nil {
62+
return nil, err
63+
}
64+
out = append(out, cEnc...)
65+
}
66+
return out, nil
67+
}
68+
69+
type bindingFactor struct {
70+
ID group.Scalar
71+
factor group.Scalar
72+
}
73+
74+
func (s Suite) getBindingFactorFromID(bindingFactors []bindingFactor, id group.Scalar) (group.Scalar, error) {
75+
for i := range bindingFactors {
76+
if bindingFactors[i].ID.IsEqual(id) {
77+
return bindingFactors[i].factor, nil
78+
}
79+
}
80+
return nil, errors.New("frost: id not found")
81+
}
82+
83+
func (s Suite) getBindingFactors(coms []*Commitment, msg []byte) ([]bindingFactor, error) {
84+
msgHash := s.hasher.h4(msg)
85+
encodeComs, err := encodeCommitments(coms)
86+
if err != nil {
87+
return nil, err
88+
}
89+
encodeComsHash := s.hasher.h5(encodeComs)
90+
rhoInputPrefix := append(msgHash, encodeComsHash...)
91+
92+
bindingFactors := make([]bindingFactor, len(coms))
93+
for i := range coms {
94+
id, err := coms[i].ID.MarshalBinary()
95+
if err != nil {
96+
return nil, err
97+
}
98+
rhoInput := append(append([]byte{}, rhoInputPrefix...), id...)
99+
bf := s.hasher.h1(rhoInput)
100+
bindingFactors[i] = bindingFactor{ID: coms[i].ID, factor: bf}
101+
}
102+
103+
return bindingFactors, nil
104+
}
105+
106+
func (s Suite) getGroupCommitment(coms []*Commitment, bindingFactors []bindingFactor) (group.Element, error) {
107+
gc := s.g.NewElement()
108+
tmp := s.g.NewElement()
109+
for i := range coms {
110+
bf, err := s.getBindingFactorFromID(bindingFactors, coms[i].ID)
111+
if err != nil {
112+
return nil, err
113+
}
114+
tmp.Mul(coms[i].binding, bf)
115+
tmp.Add(tmp, coms[i].hiding)
116+
gc.Add(gc, tmp)
117+
}
118+
119+
return gc, nil
120+
}
121+
122+
func (s Suite) getChallenge(groupCom group.Element, pubKey *PublicKey, msg []byte) (group.Scalar, error) {
123+
gcEnc, err := groupCom.MarshalBinaryCompress()
124+
if err != nil {
125+
return nil, err
126+
}
127+
pkEnc, err := pubKey.key.MarshalBinaryCompress()
128+
if err != nil {
129+
return nil, err
130+
}
131+
chInput := append(append(append([]byte{}, gcEnc...), pkEnc...), msg...)
132+
133+
return s.hasher.h2(chInput), nil
134+
}

tss/frost/doc.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
Package frost provides the FROST threshold signature scheme for Schnorr signatures.
3+
4+
References
5+
6+
FROST paper: https://eprint.iacr.org/2020/852
7+
draft-irtf-cfrg-frost: https://datatracker.ietf.org/doc/draft-irtf-cfrg-frost
8+
9+
Version supported: v11
10+
*/
11+
package frost

tss/frost/frost.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package frost
2+
3+
import (
4+
"io"
5+
6+
"go.bryk.io/circl/group"
7+
"go.bryk.io/circl/vss"
8+
)
9+
10+
type PrivateKey struct {
11+
Suite
12+
key group.Scalar
13+
pubKey *PublicKey
14+
}
15+
16+
type PublicKey struct {
17+
Suite
18+
key group.Element
19+
}
20+
21+
func GenerateKey(s Suite, rnd io.Reader) *PrivateKey {
22+
return &PrivateKey{s, s.g.RandomNonZeroScalar(rnd), nil}
23+
}
24+
25+
func (k *PrivateKey) Public() *PublicKey {
26+
return &PublicKey{k.Suite, k.Suite.g.NewElement().MulGen(k.key)}
27+
}
28+
29+
func (k *PrivateKey) Split(rnd io.Reader, threshold, maxSigners uint) (
30+
[]PeerSigner, vss.SecretCommitment, error,
31+
) {
32+
ss := vss.New(rnd, threshold, k.key)
33+
shares := ss.Share(maxSigners)
34+
35+
peers := make([]PeerSigner, len(shares))
36+
for i := range shares {
37+
peers[i] = PeerSigner{
38+
Suite: k.Suite,
39+
threshold: uint16(threshold),
40+
maxSigners: uint16(maxSigners),
41+
keyShare: vss.Share{
42+
ID: shares[i].ID,
43+
Value: shares[i].Value,
44+
},
45+
myPubKey: nil,
46+
}
47+
}
48+
49+
return peers, ss.CommitSecret(), nil
50+
}
51+
52+
func Verify(s Suite, pubKey *PublicKey, msg, signature []byte) bool {
53+
params := s.g.Params()
54+
Ne, Ns := params.CompressedElementLength, params.ScalarLength
55+
if len(signature) < int(Ne+Ns) {
56+
return false
57+
}
58+
59+
REnc := signature[:Ne]
60+
R := s.g.NewElement()
61+
err := R.UnmarshalBinary(REnc)
62+
if err != nil {
63+
return false
64+
}
65+
66+
zEnc := signature[Ne : Ne+Ns]
67+
z := s.g.NewScalar()
68+
err = z.UnmarshalBinary(zEnc)
69+
if err != nil {
70+
return false
71+
}
72+
73+
pubKeyEnc, err := pubKey.key.MarshalBinaryCompress()
74+
if err != nil {
75+
return false
76+
}
77+
78+
chInput := append(append(append([]byte{}, REnc...), pubKeyEnc...), msg...)
79+
c := s.hasher.h2(chInput)
80+
81+
l := s.g.NewElement().MulGen(z)
82+
r := s.g.NewElement().Mul(pubKey.key, c)
83+
r.Add(r, R)
84+
85+
return l.IsEqual(r)
86+
}

0 commit comments

Comments
 (0)