diff --git a/group/secretsharing/poly.go b/group/secretsharing/poly.go new file mode 100644 index 000000000..8e6b4adee --- /dev/null +++ b/group/secretsharing/poly.go @@ -0,0 +1,73 @@ +package secretsharing + +import ( + "errors" + "io" + + "github.com/cloudflare/circl/group" +) + +type polynomial struct { + deg uint64 + coeff []group.Scalar +} + +func randomPolynomial(rnd io.Reader, g group.Group, deg uint64) (p polynomial) { + p = polynomial{deg, make([]group.Scalar, deg+1)} + + for i := 0; i <= int(deg); i++ { + p.coeff[i] = g.RandomScalar(rnd) + } + return +} + +func (p polynomial) evaluate(x group.Scalar) group.Scalar { + px := p.coeff[p.deg].Copy() + for i := int(p.deg) - 1; i >= 0; i-- { + px.Mul(px, x) + px.Add(px, p.coeff[i]) + } + return px +} + +func LagrangeCoefficient(g group.Group, x []group.Scalar, index uint64) group.Scalar { + if index > uint64(len(x)) { + panic("invalid parameter") + } + + num := g.NewScalar() + num.SetUint64(1) + den := g.NewScalar() + den.SetUint64(1) + tmp := g.NewScalar() + + for j := range x { + if j != int(index) { + num.Mul(num, x[j]) + den.Mul(den, tmp.Sub(x[j], x[index])) + } + } + + return num.Mul(num, tmp.Inv(den)) +} + +func LagrangeInterpolate(g group.Group, x, px []group.Scalar) (group.Scalar, error) { + if len(x) != len(px) { + return nil, errors.New("lagrange: bad input length") + } + + zero := g.NewScalar() + for i := range x { + if x[i].IsEqual(zero) { + return nil, errors.New("lagrange: tried to evaluate on zero") + } + } + + pol0 := g.NewScalar() + delta := g.NewScalar() + for i := range x { + pol0.Add(pol0, delta.Mul(px[i], LagrangeCoefficient(g, x, uint64(i)))) + } + + return pol0, nil +} diff --git a/group/secretsharing/poly_test.go b/group/secretsharing/poly_test.go new file mode 100644 index 000000000..945ebcaf5 --- /dev/null +++ b/group/secretsharing/poly_test.go @@ -0,0 +1,60 @@ +package secretsharing + +import ( + "testing" + + "github.com/cloudflare/circl/group" + "github.com/cloudflare/circl/internal/test" +) + +func TestPolyEval(t *testing.T) { + g := group.P256 + p := polynomial{2, []group.Scalar{ + g.NewScalar(), + g.NewScalar(), + g.NewScalar(), + }} + p.coeff[0].SetUint64(5) + p.coeff[1].SetUint64(5) + p.coeff[2].SetUint64(2) + + x := g.NewScalar() + x.SetUint64(10) + + got := p.evaluate(x) + + want := g.NewScalar() + want.SetUint64(255) + if !got.IsEqual(want) { + test.ReportError(t, got, want) + } +} + +func TestLagrange(t *testing.T) { + g := group.P256 + p := polynomial{2, []group.Scalar{ + g.NewScalar(), + g.NewScalar(), + g.NewScalar(), + }} + p.coeff[0].SetUint64(1234) + p.coeff[1].SetUint64(166) + p.coeff[2].SetUint64(94) + + x := []group.Scalar{g.NewScalar(), g.NewScalar(), g.NewScalar()} + px := []group.Scalar{g.NewScalar(), g.NewScalar(), g.NewScalar()} + x[0].SetUint64(2) + px[0].SetUint64(1942) + x[1].SetUint64(4) + px[1].SetUint64(3402) + x[2].SetUint64(5) + px[2].SetUint64(4414) + + got, err := LagrangeInterpolate(g, x, px) + test.CheckNoErr(t, err, "failed interpolation") + want := p.coeff[0] + + if !got.IsEqual(want) { + test.ReportError(t, got, want) + } +} diff --git a/group/secretsharing/ss.go b/group/secretsharing/ss.go new file mode 100644 index 000000000..bd3cab84e --- /dev/null +++ b/group/secretsharing/ss.go @@ -0,0 +1,138 @@ +// Package secretsharing provides methods to split a secret in shares. +// +// Shamir's secret sharing: https://dl.acm.org/doi/10.1145/359168.359176 +// +// Feldman's verifiable secret sharing: https://ieeexplore.ieee.org/document/4568297 +package secretsharing + +import ( + "errors" + "fmt" + "io" + + "github.com/cloudflare/circl/group" +) + +type SecretShare struct { + ID uint64 + Share group.Scalar +} + +// SecretSharing implements Shamir's secret sharing. +type SecretSharing interface { + Shard(rnd io.Reader, secret group.Scalar) []SecretShare + Recover(shares []SecretShare) (secret group.Scalar, err error) +} + +type secretSharing struct { + G group.Group + T, N uint64 +} + +// New returns a struct implementing SecretSharing interface. +func New(g group.Group, t, n uint64) (secretSharing, error) { + if !(0 < t && t <= n) || g == nil { + return secretSharing{}, errors.New("secretsharing: bad parameters") + } + ss := secretSharing{G: g, T: t, N: n} + var _ SecretSharing = ss // checking at compile-time + return ss, nil +} + +func (s secretSharing) polyFromSecret(rnd io.Reader, secret group.Scalar) (p polynomial) { + p = randomPolynomial(rnd, s.G, s.T) + p.coeff[0] = secret.Copy() + return +} + +func (s secretSharing) generateShares(poly polynomial) []SecretShare { + shares := make([]SecretShare, s.N) + x := s.G.NewScalar() + for i := range shares { + id := uint64(i + 1) + x.SetUint64(id) + shares[i].ID = id + shares[i].Share = poly.evaluate(x) + } + + return shares +} + +func (s secretSharing) Shard(rnd io.Reader, secret group.Scalar) []SecretShare { + return s.generateShares(s.polyFromSecret(rnd, secret)) +} + +func (s secretSharing) Recover(shares []SecretShare) (group.Scalar, error) { + if l := len(shares); l <= int(s.T) { + return nil, fmt.Errorf("secretsharing: do not met threshold %v with %v shares", s.T, l) + } else if l > int(s.N) { + return nil, fmt.Errorf("secretsharing: %v shares above max number of shares %v", l, s.N) + } + + x := make([]group.Scalar, len(shares)) + px := make([]group.Scalar, len(shares)) + for i := range shares { + x[i] = s.G.NewScalar() + x[i].SetUint64(shares[i].ID) + px[i] = shares[i].Share + } + + return LagrangeInterpolate(s.G, x, px) +} + +type SharesCommitment = []group.Element + +type verifiableSecretSharing struct { + s secretSharing +} + +// SecretSharing implements Feldman's secret sharing. +type VerifiableSecretSharing interface { + Shard(rnd io.Reader, secret group.Scalar) ([]SecretShare, SharesCommitment) + Recover(shares []SecretShare) (secret group.Scalar, err error) + Verify(share SecretShare, coms SharesCommitment) bool +} + +// New returns a struct implementing VerifiableSecretSharing interface. +func NewVerifiable(g group.Group, t, n uint64) (verifiableSecretSharing, error) { + s, err := New(g, t, n) + vs := verifiableSecretSharing{s} + var _ verifiableSecretSharing = vs // checking at compile-time + return vs, err +} + +func (v verifiableSecretSharing) Shard(rnd io.Reader, secret group.Scalar) ([]SecretShare, SharesCommitment) { + poly := v.s.polyFromSecret(rnd, secret) + shares := v.s.generateShares(poly) + + vecComm := make(SharesCommitment, v.s.T+1) + for i, ki := range poly.coeff { + vecComm[i] = v.s.G.NewElement() + vecComm[i].MulGen(ki) + } + + return shares, vecComm +} + +func (v verifiableSecretSharing) Verify(s SecretShare, c SharesCommitment) bool { + if len(c) != int(v.s.T+1) { + return false + } + + polI := v.s.G.NewElement().MulGen(s.Share) + + lc := len(c) - 1 + sum := c[lc].Copy() + x := v.s.G.NewScalar() + for i := lc - 1; i >= 0; i-- { + x.SetUint64(s.ID) + sum.Mul(sum, x) + sum.Add(sum, c[i]) + } + + return polI.IsEqual(sum) +} + +func (v verifiableSecretSharing) Recover(shares []SecretShare) (group.Scalar, error) { + return v.s.Recover(shares) +} diff --git a/group/secretsharing/ss_test.go b/group/secretsharing/ss_test.go new file mode 100644 index 000000000..ba1b3443d --- /dev/null +++ b/group/secretsharing/ss_test.go @@ -0,0 +1,149 @@ +package secretsharing_test + +import ( + "crypto/rand" + "testing" + + "github.com/cloudflare/circl/group" + "github.com/cloudflare/circl/group/secretsharing" + "github.com/cloudflare/circl/internal/test" +) + +func TestSecretSharing(tt *testing.T) { + g := group.P256 + t := uint64(3) + n := uint64(5) + + s, err := secretsharing.New(g, t, n) + test.CheckNoErr(tt, err, "failed to create ShamirSS") + + want := g.RandomScalar(rand.Reader) + shares := s.Shard(rand.Reader, want) + test.CheckOk(len(shares) == int(n), "bad num shares", tt) + + tt.Run("subsetSize", func(ttt *testing.T) { + // Test any possible subset size. + for k := 0; k < int(n); k++ { + got, err := s.Recover(shares[:k]) + if k <= int(t) { + test.CheckIsErr(ttt, err, "should not recover secret") + test.CheckOk(got == nil, "not nil secret", ttt) + } else { + test.CheckNoErr(ttt, err, "should recover secret") + if !got.IsEqual(want) { + test.ReportError(ttt, got, want, t, k, n) + } + } + } + }) +} + +func TestVerifiableSecretSharing(tt *testing.T) { + g := group.P256 + t := uint64(3) + n := uint64(5) + + vs, err := secretsharing.NewVerifiable(g, t, n) + test.CheckNoErr(tt, err, "failed to create ShamirSS") + + want := g.RandomScalar(rand.Reader) + shares, com := vs.Shard(rand.Reader, want) + test.CheckOk(len(shares) == int(n), "bad num shares", tt) + test.CheckOk(len(com) == int(t+1), "bad num commitments", tt) + + tt.Run("verifyShares", func(ttt *testing.T) { + for i := range shares { + test.CheckOk(vs.Verify(shares[i], com) == true, "failed one share", ttt) + } + }) + + tt.Run("subsetSize", func(ttt *testing.T) { + // Test any possible subset size. + for k := 0; k < int(n); k++ { + got, err := vs.Recover(shares[:k]) + if k <= int(t) { + test.CheckIsErr(ttt, err, "should not recover secret") + test.CheckOk(got == nil, "not nil secret", ttt) + } else { + test.CheckNoErr(ttt, err, "should recover secret") + if !got.IsEqual(want) { + test.ReportError(ttt, got, want, t, k, n) + } + } + } + }) + + tt.Run("badShares", func(ttt *testing.T) { + badShares := make([]secretsharing.SecretShare, len(shares)) + for i := range shares { + badShares[i].Share = shares[i].Share.Copy() + badShares[i].Share.SetUint64(9) + } + + for i := range badShares { + test.CheckOk(vs.Verify(badShares[i], com) == false, "verify must fail due to bad shares", ttt) + } + }) + + tt.Run("badCommitments", func(ttt *testing.T) { + badCom := make(secretsharing.SharesCommitment, len(com)) + for i := range com { + badCom[i] = com[i].Copy() + badCom[i].Dbl(badCom[i]) + } + + for i := range shares { + test.CheckOk(vs.Verify(shares[i], badCom) == false, "verify must fail due to bad commitment", ttt) + } + }) +} + +func BenchmarkSecretSharing(b *testing.B) { + g := group.P256 + t := uint64(3) + n := uint64(5) + + s, _ := secretsharing.New(g, t, n) + want := g.RandomScalar(rand.Reader) + shares := s.Shard(rand.Reader, want) + + b.Run("Shard", func(b *testing.B) { + for i := 0; i < b.N; i++ { + s.Shard(rand.Reader, want) + } + }) + + b.Run("Recover", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = s.Recover(shares) + } + }) +} + +func BenchmarkVerifiableSecretSharing(b *testing.B) { + g := group.P256 + t := uint64(3) + n := uint64(5) + + vs, _ := secretsharing.NewVerifiable(g, t, n) + want := g.RandomScalar(rand.Reader) + shares, com := vs.Shard(rand.Reader, want) + + b.Run("Shard", func(b *testing.B) { + for i := 0; i < b.N; i++ { + vs.Shard(rand.Reader, want) + } + }) + + b.Run("Recover", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = vs.Recover(shares) + } + }) + + b.Run("Verify", func(b *testing.B) { + for i := 0; i < b.N; i++ { + vs.Verify(shares[0], com) + } + }) +}