diff --git a/math/polynomial/polynomial.go b/math/polynomial/polynomial.go index bee81274..275eaed5 100644 --- a/math/polynomial/polynomial.go +++ b/math/polynomial/polynomial.go @@ -33,6 +33,8 @@ func New(coeffs []group.Scalar) (p Polynomial) { return } +// Degree returns the degree of the polynomial. The zero polynomial has degree +// equal to -1. func (p Polynomial) Degree() int { i := len(p.c) - 1 for i > 0 && p.c[i].IsZero() { @@ -41,6 +43,7 @@ func (p Polynomial) Degree() int { return i } +// Evaluate returns the evaluation of p on x. func (p Polynomial) Evaluate(x group.Scalar) group.Scalar { px := x.Group().NewScalar() if l := len(p.c); l != 0 { @@ -53,6 +56,15 @@ func (p Polynomial) Evaluate(x group.Scalar) group.Scalar { return px } +// Coefficient returns a deep-copy of the n-th polynomial's coefficient. +// Note coefficients are sorted in ascending order with respect to the degree. +func (p Polynomial) Coefficient(n uint) group.Scalar { + if int(n) >= len(p.c) { + panic("polynomial: invalid index for coefficient") + } + return p.c[n].Copy() +} + // 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/example_test.go b/secretsharing/example_test.go new file mode 100644 index 00000000..1bd48d54 --- /dev/null +++ b/secretsharing/example_test.go @@ -0,0 +1,57 @@ +package secretsharing_test + +import ( + "crypto/rand" + "fmt" + + "github.com/cloudflare/circl/group" + "github.com/cloudflare/circl/secretsharing" +) + +func ExampleSecretSharing() { + g := group.P256 + t := uint(2) + n := uint(5) + + secret := g.RandomScalar(rand.Reader) + ss := secretsharing.New(rand.Reader, t, secret) + shares := ss.Share(n) + + got, err := secretsharing.Recover(t, shares[:t]) + fmt.Printf("Recover secret: %v\nError: %v\n", secret.IsEqual(got), err) + + got, err = secretsharing.Recover(t, shares[:t+1]) + fmt.Printf("Recover secret: %v\nError: %v\n", secret.IsEqual(got), err) + // Output: + // Recover secret: false + // Error: secretsharing: number of shares (n=2) must be above the threshold (t=2) + // Recover secret: true + // Error: +} + +func ExampleVerify() { + g := group.P256 + t := uint(2) + n := uint(5) + + secret := g.RandomScalar(rand.Reader) + ss := secretsharing.New(rand.Reader, t, secret) + shares := ss.Share(n) + coms := ss.CommitSecret() + + for i := range shares { + ok := secretsharing.Verify(t, shares[i], coms) + fmt.Printf("Share %v is valid: %v\n", i, ok) + } + + got, err := secretsharing.Recover(t, shares) + fmt.Printf("Recover secret: %v\nError: %v\n", secret.IsEqual(got), err) + // Output: + // Share 0 is valid: true + // Share 1 is valid: true + // Share 2 is valid: true + // Share 3 is valid: true + // Share 4 is valid: true + // Recover secret: true + // Error: +} diff --git a/secretsharing/ss.go b/secretsharing/ss.go new file mode 100644 index 00000000..f8bd7556 --- /dev/null +++ b/secretsharing/ss.go @@ -0,0 +1,145 @@ +// Package secretsharing provides methods to split secrets into shares. +// +// Let n be the number of parties, and t the number of corrupted parties such +// that 0 <= t < n. A (t,n) secret sharing allows to split a secret into n +// shares, such that the secret can be recovered from any subset of at least t+1 +// different shares. +// +// A Shamir secret sharing [1] relies on Lagrange polynomial interpolation. +// A Feldman secret sharing [2] extends Shamir's by committing the secret, +// which allows to verify that a share is part of the committed secret. +// +// New returns a SecretSharing compatible with Shamir secret sharing. +// The SecretSharing can be verifiable (compatible with Feldman secret sharing) +// using the CommitSecret and Verify functions. +// +// In this implementation, secret sharing is defined over the scalar field of +// a prime order group. +// +// References +// +// [1] Shamir, How to share a secret. https://dl.acm.org/doi/10.1145/359168.359176/ +// [2] Feldman, A practical scheme for non-interactive verifiable secret sharing. https://ieeexplore.ieee.org/document/4568297/ +package secretsharing + +import ( + "fmt" + "io" + + "github.com/cloudflare/circl/group" + "github.com/cloudflare/circl/math/polynomial" +) + +// Share represents a share of a secret. +type Share struct { + // ID uniquely identifies a share in a secret sharing instance. ID is never zero. + ID group.Scalar + // Value stores the share generated by a secret sharing instance. + Value group.Scalar +} + +// SecretCommitment is the set of commitments generated by splitting a secret. +type SecretCommitment = []group.Element + +// SecretSharing provides a (t,n) Shamir's secret sharing. It allows splitting +// a secret into n shares, such that the secret can be only recovered from +// any subset of t+1 shares. +type SecretSharing struct { + g group.Group + t uint + poly polynomial.Polynomial +} + +// New returns a SecretSharing providing a (t,n) Shamir's secret sharing. +// It allows splitting a secret into n shares, such that the secret is +// only recovered from any subset of at least t+1 shares. +func New(rnd io.Reader, t uint, secret group.Scalar) SecretSharing { + c := make([]group.Scalar, t+1) + c[0] = secret.Copy() + g := secret.Group() + for i := 1; i < len(c); i++ { + c[i] = g.RandomScalar(rnd) + } + + return SecretSharing{g: g, t: t, poly: polynomial.New(c)} +} + +// Share creates n shares with an ID monotonically increasing from 1 to n. +func (ss SecretSharing) Share(n uint) []Share { + shares := make([]Share, n) + id := ss.g.NewScalar() + for i := range shares { + shares[i] = ss.ShareWithID(id.SetUint64(uint64(i + 1))) + } + + return shares +} + +// ShareWithID creates one share of the secret using the ID as identifier. +// Notice that shares with the same ID are considered equal. +// Panics, if the ID is zero. +func (ss SecretSharing) ShareWithID(id group.Scalar) Share { + if id.IsZero() { + panic("secretsharing: id cannot be zero") + } + + return Share{ + ID: id.Copy(), + Value: ss.poly.Evaluate(id), + } +} + +// CommitSecret creates a commitment to the secret for further verifying shares. +func (ss SecretSharing) CommitSecret() SecretCommitment { + c := make(SecretCommitment, ss.poly.Degree()+1) + for i := range c { + c[i] = ss.g.NewElement().MulGen(ss.poly.Coefficient(uint(i))) + } + return c +} + +// Verify returns true if the share s was produced by sharing a secret with +// threshold t and commitment of the secret c. +func Verify(t uint, s Share, c SecretCommitment) bool { + if len(c) != int(t+1) { + return false + } + if s.ID.IsZero() { + return false + } + + g := s.ID.Group() + lc := len(c) - 1 + sum := g.NewElement().Set(c[lc]) + for i := lc - 1; i >= 0; i-- { + sum.Mul(sum, s.ID) + sum.Add(sum, c[i]) + } + polI := g.NewElement().MulGen(s.Value) + return polI.IsEqual(sum) +} + +// Recover returns a secret provided more than t different shares are given. +// Returns an error if the number of shares is not above the threshold t. +// Panics if some shares are duplicated, i.e., shares must have different IDs. +func Recover(t uint, shares []Share) (secret group.Scalar, err error) { + if l := len(shares); l <= int(t) { + return nil, errThreshold(t, uint(l)) + } + + x := make([]group.Scalar, t+1) + px := make([]group.Scalar, t+1) + for i := range shares[:t+1] { + x[i] = shares[i].ID + px[i] = shares[i].Value + } + + l := polynomial.NewLagrangePolynomial(x, px) + zero := shares[0].ID.Group().NewScalar() + + return l.Evaluate(zero), nil +} + +func errThreshold(t, n uint) error { + return fmt.Errorf("secretsharing: number of shares (n=%v) must be above the threshold (t=%v)", n, t) +} diff --git a/secretsharing/ss_test.go b/secretsharing/ss_test.go new file mode 100644 index 00000000..504e165e --- /dev/null +++ b/secretsharing/ss_test.go @@ -0,0 +1,146 @@ +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(2) + n := uint(5) + + secret := g.RandomScalar(rand.Reader) + ss := secretsharing.New(rand.Reader, t, secret) + shares := ss.Share(n) + test.CheckOk(len(shares) == int(n), "bad num shares", tt) + coms := ss.CommitSecret() + + tt.Run("subsetSize", func(ttt *testing.T) { + // Test any possible subset size. + for k := 0; k <= int(n); k++ { + got, err := secretsharing.Recover(t, shares[:k]) + if !(int(t) < k && k <= int(n)) { + test.CheckIsErr(ttt, err, "should not recover secret") + test.CheckOk(got == nil, "not nil secret", ttt) + } else { + test.CheckNoErr(ttt, err, "should recover secret") + want := secret + if !got.IsEqual(want) { + test.ReportError(ttt, got, want, t, k, n) + } + } + } + }) + + tt.Run("verifyShares", func(ttt *testing.T) { + for i := range shares { + test.CheckOk(secretsharing.Verify(t, shares[i], coms) == true, "failed one share", ttt) + } + }) + + tt.Run("badShares", func(ttt *testing.T) { + badShares := make([]secretsharing.Share, len(shares)) + for i := range shares { + badShares[i].ID = shares[i].ID.Copy() + badShares[i].Value = shares[i].Value.Copy() + badShares[i].Value.SetUint64(9) + } + + for i := range badShares { + test.CheckOk(secretsharing.Verify(t, badShares[i], coms) == false, "verify must fail due to bad shares", ttt) + } + }) + + tt.Run("badCommitments", func(ttt *testing.T) { + badComs := make(secretsharing.SecretCommitment, len(coms)) + for i := range coms { + badComs[i] = coms[i].Copy() + badComs[i].Dbl(badComs[i]) + } + + for i := range shares { + test.CheckOk(secretsharing.Verify(t, shares[i], badComs) == false, "verify must fail due to bad commitment", ttt) + } + }) +} + +func TestShareWithID(tt *testing.T) { + g := group.P256 + t := uint(2) + n := uint(5) + secret := g.RandomScalar(rand.Reader) + ss := secretsharing.New(rand.Reader, t, secret) + + tt.Run("recoverOk", func(ttt *testing.T) { + // SecretSharing can create shares at will, not exactly n many. + shares := []secretsharing.Share{ + ss.ShareWithID(g.RandomScalar(rand.Reader)), + ss.ShareWithID(g.RandomScalar(rand.Reader)), + ss.ShareWithID(g.RandomScalar(rand.Reader)), + } + got, err := secretsharing.Recover(t, shares) + test.CheckNoErr(tt, err, "failed to recover the secret") + want := secret + if !got.IsEqual(want) { + test.ReportError(tt, got, want, t, n) + } + }) + + tt.Run("duplicatedFail", func(ttt *testing.T) { + // Panics if trying to recover duplicated shares. + share := ss.ShareWithID(g.RandomScalar(rand.Reader)) + sameShares := []secretsharing.Share{share, share, share} + err := test.CheckPanic(func() { + got, err := secretsharing.Recover(t, sameShares) + test.CheckIsErr(tt, err, "must fail to recover the secret") + test.CheckOk(got == nil, "must not recover", tt) + }) + test.CheckOk(err == nil, "must panic", tt) + }) +} + +func BenchmarkSecretSharing(b *testing.B) { + g := group.P256 + t := uint(3) + n := uint(5) + + secret := g.RandomScalar(rand.Reader) + ss := secretsharing.New(rand.Reader, t, secret) + shares := ss.Share(n) + coms := ss.CommitSecret() + + b.Run("New", func(b *testing.B) { + for i := 0; i < b.N; i++ { + secretsharing.New(rand.Reader, t, secret) + } + }) + + b.Run("Share", func(b *testing.B) { + for i := 0; i < b.N; i++ { + ss.Share(n) + } + }) + + b.Run("Recover", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = secretsharing.Recover(t, shares) + } + }) + + b.Run("CommitSecret", func(b *testing.B) { + for i := 0; i < b.N; i++ { + ss.CommitSecret() + } + }) + + b.Run("Verify", func(b *testing.B) { + for i := 0; i < b.N; i++ { + secretsharing.Verify(t, shares[0], coms) + } + }) +}