Skip to content

Commit

Permalink
Adding verification of signature shares.
Browse files Browse the repository at this point in the history
  • Loading branch information
armfazh committed Jul 19, 2023
1 parent 24eb204 commit 5dcac57
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 26 deletions.
10 changes: 6 additions & 4 deletions tss/rsa/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
This is an implementation of ["Practical Threshold Signatures" by Victor Shoup](https://www.iacr.org/archive/eurocrypt2000/1807/18070209-new.pdf).
Protocol 1 is implemented.

## Threshold Primer
## Threshold Cryptography Primer

Let *l* be the total number of players, *t* be the number of corrupted players, and *k* be the threshold.
The idea of threshold signatures is that at least *k* players need to participate to form a valid signature.
Expand All @@ -13,7 +13,9 @@ Setup consists of a dealer generating *l* key shares from a key pair and "dealin
During the signing phase, at least *k* players use their key share and the message to generate a signature share.
Finally, the *k* signature shares are combined to form a valid signature for the message.

## Modifications
## Robustness

1. Our implementation is not robust. That is, the corrupted players can prevent a valid signature from being formed by the non-corrupted players. As such, we remove all verification.
2. The paper requires p and q to be safe primes. We do not.
The scheme requires p and q to be safe primes to provide robustness. That is, it is possible to validate (and reject) signature shares produced by malicious signers. RSA keys generated by the Go standard library are not guaranteed to be safe primes. In this case, the functions produces signature shares but they are not verifiable.
To provide verifiability, use the GenerateKey function in this package, which produces a key pair composed of safe primes.

The Deal function opportunistically checks whether the RSA key is composed of safe primes, if so, the signature shares produced are verifiable.
56 changes: 56 additions & 0 deletions tss/rsa/keyshare.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,19 @@ import (
"math"
"math/big"
"sync"

"github.com/cloudflare/circl/zk/qndleq"
)

// VerifyKeys contains keys used to verify whether a signature share
// was computed using the signer's key share.
type VerifyKeys struct {
// This key is common to the group of signers.
GroupKey *big.Int
// This key is the (public) key associated with the (private) key share.
VerifyKey *big.Int
}

// KeyShare represents a portion of the key. It can only be used to generate SignShare's. During the dealing phase (when Deal is called), one KeyShare is generated per player.
type KeyShare struct {
si *big.Int
Expand All @@ -21,6 +32,13 @@ type KeyShare struct {

Players uint
Threshold uint

// It stores keys to produce verifiable signature shares.
// If it's nil, signature shares are still produced but
// they are not verifiable.
// This field is present only if the RSA private key is
// composed of two safe primes.
vk *VerifyKeys
}

func (kshare KeyShare) String() string {
Expand Down Expand Up @@ -173,6 +191,24 @@ func (kshare *KeyShare) get2DeltaSi(players int64) *big.Int {
return delta
}

// IsVerifiable returns true if the key share can produce
// verifiable signature shares.
func (kshare *KeyShare) IsVerifiable() bool { return kshare.vk != nil }

// VerifyKeys returns a copy of the verification keys used to verify
// signature shares. Returns nil if the key share cannot produce
// verifiable signature shares.
func (kshare *KeyShare) VerifyKeys() (vk *VerifyKeys) {
if kshare.IsVerifiable() {
vk = &VerifyKeys{
GroupKey: new(big.Int).Set(kshare.vk.GroupKey),
VerifyKey: new(big.Int).Set(kshare.vk.VerifyKey),
}
}

return
}

// Sign msg using a KeyShare. msg MUST be padded and hashed. Call PadHash before this method.
//
// If rand is not nil then blinding will be used to avoid timing
Expand Down Expand Up @@ -248,5 +284,25 @@ func (kshare *KeyShare) Sign(randSource io.Reader, pub *rsa.PublicKey, digest []
signShare.xi.Exp(x, exp, pub.N)
}

// When verification keys are available, a DLEQ Proof is included.
if kshare.vk != nil {
const SecParam = 128
fourDelta := calculateDelta(int64(kshare.Players))
fourDelta.Lsh(fourDelta, 2)
x4Delta := new(big.Int).Exp(x, fourDelta, pub.N)
xiSqr := new(big.Int).Mul(signShare.xi, signShare.xi)
xiSqr.Mod(xiSqr, pub.N)

proof, err := qndleq.Prove(randSource,
kshare.si,
kshare.vk.GroupKey, kshare.vk.VerifyKey,
x4Delta, xiSqr,
pub.N, SecParam)
if err != nil {
return SignShare{}, err
}
signShare.proof = proof
}

return signShare, nil
}
2 changes: 1 addition & 1 deletion tss/rsa/keyshare_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ func TestMarshallKeyShareFull(t *testing.T) {
if err != nil {
t.Fatal(err)
}
keys, err := Deal(rand.Reader, players, threshold, key, false)
keys, err := Deal(rand.Reader, players, threshold, key)
if err != nil {
t.Fatal(err)
}
Expand Down
43 changes: 32 additions & 11 deletions tss/rsa/rsa_threshold.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@ import (
"math/big"

cmath "github.com/cloudflare/circl/math"
"github.com/cloudflare/circl/zk/qndleq"
)

// GenerateKey generates a RSA keypair for its use in RSA threshold signatures.
// Internally, the modulus is the product of two safe primes. The time
// consumed by this function is relatively longer than the regular
// GenerateKey function from the crypto/rsa package.
// GenerateKey generates an RSA keypair for its use in RSA threshold signatures.
// Unlike crypto/rsa.GenerateKey, this function calculates the modulus as the
// product of two safe primes. Note that the time consumed by this function is
// relatively longer than the time of the crypto/rsa.GenerateKey function.
//
// Generate keys with this function to enable verifiability of signature shares.
func GenerateKey(random io.Reader, bits int) (*rsa.PrivateKey, error) {
p, err := cmath.SafePrime(random, bits/2)
if err != nil {
Expand Down Expand Up @@ -85,9 +88,12 @@ func validateParams(players, threshold uint) error {
return nil
}

// Deal takes in an existing RSA private key generated elsewhere. If cache is true, cached values are stored in KeyShare taking up more memory by reducing Sign time.
// See KeyShare documentation. Multi-prime RSA keys are unsupported.
func Deal(randSource io.Reader, players, threshold uint, key *rsa.PrivateKey, cache bool) ([]KeyShare, error) {
// Deal splits an RSA private key into key shares, so signing can be performed
// from a threshold number of signatures shares.
// When the modulus is the product of two safe primes, key shares include
// keys for verification of signatures shares.
// Note that multi-prime RSA keys are not supported.
func Deal(randSource io.Reader, players, threshold uint, key *rsa.PrivateKey) ([]KeyShare, error) {
err := validateParams(players, threshold)

ONE := big.NewInt(1)
Expand All @@ -103,6 +109,7 @@ func Deal(randSource io.Reader, players, threshold uint, key *rsa.PrivateKey, ca
p := key.Primes[0]
q := key.Primes[1]
e := int64(key.E)
hasSafePrimes := cmath.IsSafePrime(p) && cmath.IsSafePrime(q)

// p = 2p' + 1
// q = 2q' + 1
Expand Down Expand Up @@ -143,18 +150,32 @@ func Deal(randSource io.Reader, players, threshold uint, key *rsa.PrivateKey, ca
}
}

var groupKey *big.Int
if hasSafePrimes {
groupKey, err = qndleq.SampleQn(randSource, key.N)
if err != nil {
return nil, err
}
}

shares := make([]KeyShare, players)

// 1 <= i <= l
for i := uint(1); i <= players; i++ {
shares[i-1].Players = players
shares[i-1].Threshold = threshold
// Σ^{k-1}_{i=0} | a_i * X^i (mod m)
poly := computePolynomial(threshold, a, i, &m)
shares[i-1].si = poly
si := computePolynomial(threshold, a, i, &m)
shares[i-1].si = si
shares[i-1].Index = i
if cache {
shares[i-1].get2DeltaSi(int64(players))
shares[i-1].get2DeltaSi(int64(players))

// If the modulus is composed by safe primes, verification keys are included.
if hasSafePrimes {
shares[i-1].vk = &VerifyKeys{
GroupKey: groupKey,
VerifyKey: new(big.Int).Exp(groupKey, si, key.N),
}
}
}

Expand Down
29 changes: 20 additions & 9 deletions tss/rsa/rsa_threshold_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func createPrivateKey(p, q *big.Int, e int) *rsa.PrivateKey {
return &rsa.PrivateKey{
PublicKey: rsa.PublicKey{
E: e,
N: new(big.Int).Mul(p, q),
},
D: nil,
Primes: []*big.Int{p, q},
Expand Down Expand Up @@ -141,7 +142,7 @@ func TestDeal(t *testing.T) {
//
//
//
r := bytes.NewReader([]byte{33, 17})
r := io.MultiReader(bytes.NewReader([]byte{33, 17}), rand.Reader)
players := uint(3)
threshold := uint(2)
p := int64(23)
Expand All @@ -150,7 +151,7 @@ func TestDeal(t *testing.T) {

key := createPrivateKey(big.NewInt(p), big.NewInt(q), e)

share, err := Deal(r, players, threshold, key, false)
share, err := Deal(r, players, threshold, key)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -198,6 +199,14 @@ func testIntegration(t *testing.T, algo crypto.Hash, priv *rsa.PrivateKey, thres
if err != nil {
t.Fatal(err)
}

if signshares[i].IsVerifiable() {
verifKeys := keys[i].VerifyKeys()
err = signshares[i].Verify(pub, verifKeys, msgPH)
if err != nil {
t.Fatalf("sign share is verifiable, but didn't pass verification")
}
}
}

sig, err := CombineSignShares(pub, signshares, msgPH)
Expand Down Expand Up @@ -236,14 +245,15 @@ func testIntegration(t *testing.T, algo crypto.Hash, priv *rsa.PrivateKey, thres
func TestIntegrationStdRsaKeyGenerationPKS1v15(t *testing.T) {
const players = 3
const threshold = 2
const bits = 2048
// [warning] Bitlength used for tests only, use a bitlength above 2048 for security.
const bits = 512
const algo = crypto.SHA256

key, err := rsa.GenerateKey(rand.Reader, bits)
key, err := GenerateKey(rand.Reader, bits)
if err != nil {
t.Fatal(err)
}
keys, err := Deal(rand.Reader, players, threshold, key, false)
keys, err := Deal(rand.Reader, players, threshold, key)
if err != nil {
t.Fatal(err)
}
Expand All @@ -253,14 +263,15 @@ func TestIntegrationStdRsaKeyGenerationPKS1v15(t *testing.T) {
func TestIntegrationStdRsaKeyGenerationPSS(t *testing.T) {
const players = 3
const threshold = 2
const bits = 2048
// [warning] Bitlength used for tests only, use a bitlength above 2048 for security.
const bits = 512
const algo = crypto.SHA256

key, err := rsa.GenerateKey(rand.Reader, bits)
if err != nil {
t.Fatal(err)
}
keys, err := Deal(rand.Reader, players, threshold, key, false)
keys, err := Deal(rand.Reader, players, threshold, key)
if err != nil {
t.Fatal(err)
}
Expand All @@ -275,7 +286,7 @@ func benchmarkSignCombineHelper(randSource io.Reader, parallel bool, b *testing.
panic(err)
}

keys, err := Deal(rand.Reader, players, threshold, key, true)
keys, err := Deal(rand.Reader, players, threshold, key)
if err != nil {
b.Fatal(err)
}
Expand Down Expand Up @@ -428,7 +439,7 @@ func BenchmarkDealGeneration(b *testing.B) {
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := Deal(rand.Reader, players, threshold, key, false)
_, err := Deal(rand.Reader, players, threshold, key)
if err != nil {
b.Fatal(err)
}
Expand Down
48 changes: 48 additions & 0 deletions tss/rsa/signShare.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package rsa

import (
"crypto/rsa"
"encoding/binary"
"errors"
"fmt"
"math"
"math/big"

"github.com/cloudflare/circl/zk/qndleq"
)

// SignShare represents a portion of a signature. It is generated when a message is signed by a KeyShare. t SignShare's are then combined by calling CombineSignShares, where t is the Threshold.
Expand All @@ -15,13 +19,52 @@ type SignShare struct {

Players uint
Threshold uint

// It stores a DLEQ proof attesting that the signature
// share was computed using the signer's key share.
// If it's nil, signature share is not verifiable.
// This field is present only if the RSA private key is
// composed of two safe primes.
proof *qndleq.Proof
}

func (s SignShare) String() string {
return fmt.Sprintf("(t,n): (%v,%v) index: %v xi: 0x%v",
s.Threshold, s.Players, s.Index, s.xi.Text(16))
}

// IsVerifiable returns true if the signature share contains
// a DLEQ proof for verification.
func (s *SignShare) IsVerifiable() bool { return s.proof != nil }

// Verify returns nil if the signature share is verifiable and validates
// the DLEQ proof. This indicates the signature share of the message was
// produced using the signer's key share. The signer must provide its
// verification keys. If proof verification does not pass, returns
// an ErrSignShareInvalid error.
//
// Before calling this function, ensure the signature share is verifiable
// by calling the method IsVerifiable. If the signature share is not
// verifiable, this function returns an ErrSignShareNonVerifiable error.
func (s *SignShare) Verify(pub *rsa.PublicKey, vk *VerifyKeys, digest []byte) error {
if !s.IsVerifiable() {
return ErrSignShareNonVerifiable
}

x := new(big.Int).SetBytes(digest)
fourDelta := calculateDelta(int64(s.Players))
fourDelta.Lsh(fourDelta, 2)
x4Delta := new(big.Int).Exp(x, fourDelta, pub.N)
xiSqr := new(big.Int).Mul(s.xi, s.xi)
xiSqr.Mod(xiSqr, pub.N)

if !s.proof.Verify(vk.GroupKey, vk.VerifyKey, x4Delta, xiSqr, pub.N) {
return ErrSignShareInvalid
}

return nil
}

// MarshalBinary encodes SignShare into a byte array in a format readable by UnmarshalBinary.
// Note: Only Index's up to math.MaxUint16 are supported
func (s *SignShare) MarshalBinary() ([]byte, error) {
Expand Down Expand Up @@ -101,3 +144,8 @@ func (s *SignShare) UnmarshalBinary(data []byte) error {

return nil
}

var (
ErrSignShareNonVerifiable = errors.New("signature share is not verifiable")
ErrSignShareInvalid = errors.New("signature share is invalid")
)
2 changes: 1 addition & 1 deletion tss/rsa/signShare_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func TestMarshallFullSignShare(t *testing.T) {
if err != nil {
t.Fatal(err)
}
keys, err := Deal(rand.Reader, players, threshold, key, false)
keys, err := Deal(rand.Reader, players, threshold, key)
if err != nil {
t.Fatal(err)
}
Expand Down

0 comments on commit 5dcac57

Please sign in to comment.