diff --git a/math/polynomial/polynomial.go b/math/polynomial/polynomial.go index 2cce1bb9d..d638e7fc5 100644 --- a/math/polynomial/polynomial.go +++ b/math/polynomial/polynomial.go @@ -51,6 +51,14 @@ func (p Polynomial) Evaluate(x group.Scalar) group.Scalar { return px } +func (p Polynomial) Coefficients() []group.Scalar { + c := make([]group.Scalar, len(p.c)) + for i := range p.c { + c[i] = p.c[i].Copy() + } + return c +} + // LagrangePolynomial stores a Lagrange polynomial over the set of scalars of a group. type LagrangePolynomial struct { // Internal representation is in Lagrange basis: diff --git a/secretsharing/ss.go b/secretsharing/ss.go new file mode 100644 index 000000000..f7d4b75f8 --- /dev/null +++ b/secretsharing/ss.go @@ -0,0 +1,171 @@ +// Package secretsharing provides methods to split secrets in shares. +// +// A (t,n) secret sharing allows to split a secret into n shares, such that the +// secret can be only recovered given more than t shares. +// +// The New function creates a Shamir secret sharing [1], which relies on +// Lagrange polynomial interpolation. +// +// The NewVerifiable function creates a Feldman secret sharing [2], which +// extends Shamir's by allowing to verify that a share corresponds to the +// secret. +// +// References +// [1] https://dl.acm.org/doi/10.1145/359168.359176 +// [2] https://ieeexplore.ieee.org/document/4568297 +package secretsharing + +import ( + "errors" + "fmt" + "io" + + "github.com/cloudflare/circl/group" + "github.com/cloudflare/circl/math/polynomial" +) + +// Share represents a share of a secret. +type Share struct { + ID uint + Share group.Scalar +} + +// SecretSharing implements a (t,n) Shamir's secret sharing. +type SecretSharing interface { + // Params returns the t and n parameters of the secret sharing. + Params() (t, n uint) + // Shard splits the secret into n shares. + Shard(rnd io.Reader, secret group.Scalar) []Share + // Recover returns the secret provided more than t shares are given. + Recover(shares []Share) (secret group.Scalar, err error) +} + +type ss struct { + g group.Group + t, n uint +} + +// New returns a struct implementing SecretSharing interface. A (t,n) secret +// sharing allows to split a secret into n shares, such that the secret can be +// only recovered given more than t shares. It panics if 0 < t <= n does not +// hold. +func New(g group.Group, t, n uint) (ss, error) { + if !(0 < t && t <= n) { + return ss{}, errors.New("secretsharing: bad parameters") + } + s := ss{g: g, t: t, n: n} + var _ SecretSharing = s // checking at compile-time + return s, nil +} + +func (s ss) Params() (t, n uint) { return s.t, s.n } + +func (s ss) polyFromSecret(rnd io.Reader, secret group.Scalar) (p polynomial.Polynomial) { + c := make([]group.Scalar, s.t+1) + for i := range c { + c[i] = s.g.RandomScalar(rnd) + } + c[0].Set(secret) + return polynomial.New(c) +} + +func (s ss) generateShares(poly polynomial.Polynomial) []Share { + shares := make([]Share, s.n) + x := s.g.NewScalar() + for i := range shares { + id := i + 1 + x.SetUint64(uint64(id)) + shares[i].ID = uint(id) + shares[i].Share = poly.Evaluate(x) + } + + return shares +} + +func (s ss) Shard(rnd io.Reader, secret group.Scalar) []Share { + return s.generateShares(s.polyFromSecret(rnd, secret)) +} + +func (s ss) Recover(shares []Share) (group.Scalar, error) { + if l := len(shares); l <= int(s.t) { + return nil, fmt.Errorf("secretsharing: does not reach the 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(uint64(shares[i].ID)) + px[i] = shares[i].Share + } + + l := polynomial.NewLagrangePolynomial(x, px) + zero := s.g.NewScalar() + + return l.Evaluate(zero), nil +} + +type SharesCommitment = []group.Element + +type vss struct{ s ss } + +// SecretSharing implements a (t,n) Feldman's secret sharing. +type VerifiableSecretSharing interface { + // Params returns the t and n parameters of the secret sharing. + Params() (t, n uint) + // Shard splits the secret into n shares, and a commitment of the secret + // and the shares. + Shard(rnd io.Reader, secret group.Scalar) ([]Share, SharesCommitment) + // Recover returns the secret provided more than t shares are given. + Recover(shares []Share) (secret group.Scalar, err error) + // Verify returns true if the share corresponds to a committed secret using + // the commitment produced by Shard. + Verify(share Share, coms SharesCommitment) bool +} + +// New returns a struct implementing VerifiableSecretSharing interface. A (t,n) +// secret sharing allows to split a secret into n shares, such that the secret +// can be only recovered given more than t shares. It is possible to verify +// whether a share corresponds to a secret. It panics if 0 < t <= n does not +// hold. +func NewVerifiable(g group.Group, t, n uint) (vss, error) { + s, err := New(g, t, n) + v := vss{s} + var _ VerifiableSecretSharing = v // checking at compile-time + return v, err +} + +func (v vss) Params() (t, n uint) { return v.s.Params() } + +func (v vss) Shard(rnd io.Reader, secret group.Scalar) ([]Share, SharesCommitment) { + poly := v.s.polyFromSecret(rnd, secret) + shares := v.s.generateShares(poly) + coeffs := poly.Coefficients() + shareComs := make(SharesCommitment, len(coeffs)) + for i := range coeffs { + shareComs[i] = v.s.g.NewElement().MulGen(coeffs[i]) + } + + return shares, shareComs +} + +func (v vss) Verify(s Share, c SharesCommitment) bool { + if len(c) != int(v.s.t+1) { + return false + } + + lc := len(c) - 1 + sum := v.s.g.NewElement().Set(c[lc]) + x := v.s.g.NewScalar() + for i := lc - 1; i >= 0; i-- { + x.SetUint64(uint64(s.ID)) + sum.Mul(sum, x) + sum.Add(sum, c[i]) + } + polI := v.s.g.NewElement().MulGen(s.Share) + return polI.IsEqual(sum) +} + +func (v vss) Recover(shares []Share) (group.Scalar, error) { return v.s.Recover(shares) } diff --git a/secretsharing/ss_test.go b/secretsharing/ss_test.go new file mode 100644 index 000000000..af10d0888 --- /dev/null +++ b/secretsharing/ss_test.go @@ -0,0 +1,149 @@ +package secretsharing_test + +import ( + "crypto/rand" + "testing" + + "github.com/cloudflare/circl/group" + "github.com/cloudflare/circl/internal/test" + "github.com/cloudflare/circl/secretsharing" +) + +func TestSecretSharing(tt *testing.T) { + g := group.P256 + t := uint(3) + n := uint(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 := uint(3) + n := uint(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.Share, 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 := uint(3) + n := uint(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 := uint(3) + n := uint(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) + } + }) +}