Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

group: Implements Shamir and Feldman secret sharing. #348

Merged
merged 1 commit into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions math/polynomial/polynomial.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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 {
Expand All @@ -53,6 +56,15 @@ func (p Polynomial) Evaluate(x group.Scalar) group.Scalar {
return px
}

armfazh marked this conversation as resolved.
Show resolved Hide resolved
// 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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems strange that this function takes a uint as input but Degree() outputs an int -- should they use the same type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The odd case is Degree() returning -1 for the special case of the zero polynomial. We might remove this special case.
@bwesterb what do you think?

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:
Expand Down
57 changes: 57 additions & 0 deletions secretsharing/example_test.go
Original file line number Diff line number Diff line change
@@ -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: <nil>
}

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: <nil>
}
145 changes: 145 additions & 0 deletions secretsharing/ss.go
Original file line number Diff line number Diff line change
@@ -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.
//
armfazh marked this conversation as resolved.
Show resolved Hide resolved
// 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename this to something else? Maybe Input, and maybe rename Value to Output?

// 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),
}
armfazh marked this conversation as resolved.
Show resolved Hide resolved
}

// 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)
}
146 changes: 146 additions & 0 deletions secretsharing/ss_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}