Skip to content

Commit

Permalink
Implements Shamir and Feldman secret sharing.
Browse files Browse the repository at this point in the history
  • Loading branch information
armfazh committed Feb 14, 2024
1 parent 59e26d0 commit 2a2b195
Show file tree
Hide file tree
Showing 4 changed files with 360 additions and 0 deletions.
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
}

// 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:
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.
//
// 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)
}
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)
}
})
}

0 comments on commit 2a2b195

Please sign in to comment.