Skip to content

Commit 5dcac57

Browse files
committed
Adding verification of signature shares.
1 parent 24eb204 commit 5dcac57

7 files changed

+164
-26
lines changed

tss/rsa/README.md

+6-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
This is an implementation of ["Practical Threshold Signatures" by Victor Shoup](https://www.iacr.org/archive/eurocrypt2000/1807/18070209-new.pdf).
44
Protocol 1 is implemented.
55

6-
## Threshold Primer
6+
## Threshold Cryptography Primer
77

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

16-
## Modifications
16+
## Robustness
1717

18-
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.
19-
2. The paper requires p and q to be safe primes. We do not.
18+
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.
19+
To provide verifiability, use the GenerateKey function in this package, which produces a key pair composed of safe primes.
20+
21+
The Deal function opportunistically checks whether the RSA key is composed of safe primes, if so, the signature shares produced are verifiable.

tss/rsa/keyshare.go

+56
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,19 @@ import (
1010
"math"
1111
"math/big"
1212
"sync"
13+
14+
"github.com/cloudflare/circl/zk/qndleq"
1315
)
1416

17+
// VerifyKeys contains keys used to verify whether a signature share
18+
// was computed using the signer's key share.
19+
type VerifyKeys struct {
20+
// This key is common to the group of signers.
21+
GroupKey *big.Int
22+
// This key is the (public) key associated with the (private) key share.
23+
VerifyKey *big.Int
24+
}
25+
1526
// 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.
1627
type KeyShare struct {
1728
si *big.Int
@@ -21,6 +32,13 @@ type KeyShare struct {
2132

2233
Players uint
2334
Threshold uint
35+
36+
// It stores keys to produce verifiable signature shares.
37+
// If it's nil, signature shares are still produced but
38+
// they are not verifiable.
39+
// This field is present only if the RSA private key is
40+
// composed of two safe primes.
41+
vk *VerifyKeys
2442
}
2543

2644
func (kshare KeyShare) String() string {
@@ -173,6 +191,24 @@ func (kshare *KeyShare) get2DeltaSi(players int64) *big.Int {
173191
return delta
174192
}
175193

194+
// IsVerifiable returns true if the key share can produce
195+
// verifiable signature shares.
196+
func (kshare *KeyShare) IsVerifiable() bool { return kshare.vk != nil }
197+
198+
// VerifyKeys returns a copy of the verification keys used to verify
199+
// signature shares. Returns nil if the key share cannot produce
200+
// verifiable signature shares.
201+
func (kshare *KeyShare) VerifyKeys() (vk *VerifyKeys) {
202+
if kshare.IsVerifiable() {
203+
vk = &VerifyKeys{
204+
GroupKey: new(big.Int).Set(kshare.vk.GroupKey),
205+
VerifyKey: new(big.Int).Set(kshare.vk.VerifyKey),
206+
}
207+
}
208+
209+
return
210+
}
211+
176212
// Sign msg using a KeyShare. msg MUST be padded and hashed. Call PadHash before this method.
177213
//
178214
// If rand is not nil then blinding will be used to avoid timing
@@ -248,5 +284,25 @@ func (kshare *KeyShare) Sign(randSource io.Reader, pub *rsa.PublicKey, digest []
248284
signShare.xi.Exp(x, exp, pub.N)
249285
}
250286

287+
// When verification keys are available, a DLEQ Proof is included.
288+
if kshare.vk != nil {
289+
const SecParam = 128
290+
fourDelta := calculateDelta(int64(kshare.Players))
291+
fourDelta.Lsh(fourDelta, 2)
292+
x4Delta := new(big.Int).Exp(x, fourDelta, pub.N)
293+
xiSqr := new(big.Int).Mul(signShare.xi, signShare.xi)
294+
xiSqr.Mod(xiSqr, pub.N)
295+
296+
proof, err := qndleq.Prove(randSource,
297+
kshare.si,
298+
kshare.vk.GroupKey, kshare.vk.VerifyKey,
299+
x4Delta, xiSqr,
300+
pub.N, SecParam)
301+
if err != nil {
302+
return SignShare{}, err
303+
}
304+
signShare.proof = proof
305+
}
306+
251307
return signShare, nil
252308
}

tss/rsa/keyshare_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ func TestMarshallKeyShareFull(t *testing.T) {
151151
if err != nil {
152152
t.Fatal(err)
153153
}
154-
keys, err := Deal(rand.Reader, players, threshold, key, false)
154+
keys, err := Deal(rand.Reader, players, threshold, key)
155155
if err != nil {
156156
t.Fatal(err)
157157
}

tss/rsa/rsa_threshold.go

+32-11
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@ import (
1919
"math/big"
2020

2121
cmath "github.com/cloudflare/circl/math"
22+
"github.com/cloudflare/circl/zk/qndleq"
2223
)
2324

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

88-
// 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.
89-
// See KeyShare documentation. Multi-prime RSA keys are unsupported.
90-
func Deal(randSource io.Reader, players, threshold uint, key *rsa.PrivateKey, cache bool) ([]KeyShare, error) {
91+
// Deal splits an RSA private key into key shares, so signing can be performed
92+
// from a threshold number of signatures shares.
93+
// When the modulus is the product of two safe primes, key shares include
94+
// keys for verification of signatures shares.
95+
// Note that multi-prime RSA keys are not supported.
96+
func Deal(randSource io.Reader, players, threshold uint, key *rsa.PrivateKey) ([]KeyShare, error) {
9197
err := validateParams(players, threshold)
9298

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

107114
// p = 2p' + 1
108115
// q = 2q' + 1
@@ -143,18 +150,32 @@ func Deal(randSource io.Reader, players, threshold uint, key *rsa.PrivateKey, ca
143150
}
144151
}
145152

153+
var groupKey *big.Int
154+
if hasSafePrimes {
155+
groupKey, err = qndleq.SampleQn(randSource, key.N)
156+
if err != nil {
157+
return nil, err
158+
}
159+
}
160+
146161
shares := make([]KeyShare, players)
147162

148163
// 1 <= i <= l
149164
for i := uint(1); i <= players; i++ {
150165
shares[i-1].Players = players
151166
shares[i-1].Threshold = threshold
152167
// Σ^{k-1}_{i=0} | a_i * X^i (mod m)
153-
poly := computePolynomial(threshold, a, i, &m)
154-
shares[i-1].si = poly
168+
si := computePolynomial(threshold, a, i, &m)
169+
shares[i-1].si = si
155170
shares[i-1].Index = i
156-
if cache {
157-
shares[i-1].get2DeltaSi(int64(players))
171+
shares[i-1].get2DeltaSi(int64(players))
172+
173+
// If the modulus is composed by safe primes, verification keys are included.
174+
if hasSafePrimes {
175+
shares[i-1].vk = &VerifyKeys{
176+
GroupKey: groupKey,
177+
VerifyKey: new(big.Int).Exp(groupKey, si, key.N),
178+
}
158179
}
159180
}
160181

tss/rsa/rsa_threshold_test.go

+20-9
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func createPrivateKey(p, q *big.Int, e int) *rsa.PrivateKey {
2929
return &rsa.PrivateKey{
3030
PublicKey: rsa.PublicKey{
3131
E: e,
32+
N: new(big.Int).Mul(p, q),
3233
},
3334
D: nil,
3435
Primes: []*big.Int{p, q},
@@ -141,7 +142,7 @@ func TestDeal(t *testing.T) {
141142
//
142143
//
143144
//
144-
r := bytes.NewReader([]byte{33, 17})
145+
r := io.MultiReader(bytes.NewReader([]byte{33, 17}), rand.Reader)
145146
players := uint(3)
146147
threshold := uint(2)
147148
p := int64(23)
@@ -150,7 +151,7 @@ func TestDeal(t *testing.T) {
150151

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

153-
share, err := Deal(r, players, threshold, key, false)
154+
share, err := Deal(r, players, threshold, key)
154155
if err != nil {
155156
t.Fatal(err)
156157
}
@@ -198,6 +199,14 @@ func testIntegration(t *testing.T, algo crypto.Hash, priv *rsa.PrivateKey, thres
198199
if err != nil {
199200
t.Fatal(err)
200201
}
202+
203+
if signshares[i].IsVerifiable() {
204+
verifKeys := keys[i].VerifyKeys()
205+
err = signshares[i].Verify(pub, verifKeys, msgPH)
206+
if err != nil {
207+
t.Fatalf("sign share is verifiable, but didn't pass verification")
208+
}
209+
}
201210
}
202211

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

242-
key, err := rsa.GenerateKey(rand.Reader, bits)
252+
key, err := GenerateKey(rand.Reader, bits)
243253
if err != nil {
244254
t.Fatal(err)
245255
}
246-
keys, err := Deal(rand.Reader, players, threshold, key, false)
256+
keys, err := Deal(rand.Reader, players, threshold, key)
247257
if err != nil {
248258
t.Fatal(err)
249259
}
@@ -253,14 +263,15 @@ func TestIntegrationStdRsaKeyGenerationPKS1v15(t *testing.T) {
253263
func TestIntegrationStdRsaKeyGenerationPSS(t *testing.T) {
254264
const players = 3
255265
const threshold = 2
256-
const bits = 2048
266+
// [warning] Bitlength used for tests only, use a bitlength above 2048 for security.
267+
const bits = 512
257268
const algo = crypto.SHA256
258269

259270
key, err := rsa.GenerateKey(rand.Reader, bits)
260271
if err != nil {
261272
t.Fatal(err)
262273
}
263-
keys, err := Deal(rand.Reader, players, threshold, key, false)
274+
keys, err := Deal(rand.Reader, players, threshold, key)
264275
if err != nil {
265276
t.Fatal(err)
266277
}
@@ -275,7 +286,7 @@ func benchmarkSignCombineHelper(randSource io.Reader, parallel bool, b *testing.
275286
panic(err)
276287
}
277288

278-
keys, err := Deal(rand.Reader, players, threshold, key, true)
289+
keys, err := Deal(rand.Reader, players, threshold, key)
279290
if err != nil {
280291
b.Fatal(err)
281292
}
@@ -428,7 +439,7 @@ func BenchmarkDealGeneration(b *testing.B) {
428439
}
429440
b.ResetTimer()
430441
for i := 0; i < b.N; i++ {
431-
_, err := Deal(rand.Reader, players, threshold, key, false)
442+
_, err := Deal(rand.Reader, players, threshold, key)
432443
if err != nil {
433444
b.Fatal(err)
434445
}

tss/rsa/signShare.go

+48
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package rsa
22

33
import (
4+
"crypto/rsa"
45
"encoding/binary"
6+
"errors"
57
"fmt"
68
"math"
79
"math/big"
10+
11+
"github.com/cloudflare/circl/zk/qndleq"
812
)
913

1014
// 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.
@@ -15,13 +19,52 @@ type SignShare struct {
1519

1620
Players uint
1721
Threshold uint
22+
23+
// It stores a DLEQ proof attesting that the signature
24+
// share was computed using the signer's key share.
25+
// If it's nil, signature share is not verifiable.
26+
// This field is present only if the RSA private key is
27+
// composed of two safe primes.
28+
proof *qndleq.Proof
1829
}
1930

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

36+
// IsVerifiable returns true if the signature share contains
37+
// a DLEQ proof for verification.
38+
func (s *SignShare) IsVerifiable() bool { return s.proof != nil }
39+
40+
// Verify returns nil if the signature share is verifiable and validates
41+
// the DLEQ proof. This indicates the signature share of the message was
42+
// produced using the signer's key share. The signer must provide its
43+
// verification keys. If proof verification does not pass, returns
44+
// an ErrSignShareInvalid error.
45+
//
46+
// Before calling this function, ensure the signature share is verifiable
47+
// by calling the method IsVerifiable. If the signature share is not
48+
// verifiable, this function returns an ErrSignShareNonVerifiable error.
49+
func (s *SignShare) Verify(pub *rsa.PublicKey, vk *VerifyKeys, digest []byte) error {
50+
if !s.IsVerifiable() {
51+
return ErrSignShareNonVerifiable
52+
}
53+
54+
x := new(big.Int).SetBytes(digest)
55+
fourDelta := calculateDelta(int64(s.Players))
56+
fourDelta.Lsh(fourDelta, 2)
57+
x4Delta := new(big.Int).Exp(x, fourDelta, pub.N)
58+
xiSqr := new(big.Int).Mul(s.xi, s.xi)
59+
xiSqr.Mod(xiSqr, pub.N)
60+
61+
if !s.proof.Verify(vk.GroupKey, vk.VerifyKey, x4Delta, xiSqr, pub.N) {
62+
return ErrSignShareInvalid
63+
}
64+
65+
return nil
66+
}
67+
2568
// MarshalBinary encodes SignShare into a byte array in a format readable by UnmarshalBinary.
2669
// Note: Only Index's up to math.MaxUint16 are supported
2770
func (s *SignShare) MarshalBinary() ([]byte, error) {
@@ -101,3 +144,8 @@ func (s *SignShare) UnmarshalBinary(data []byte) error {
101144

102145
return nil
103146
}
147+
148+
var (
149+
ErrSignShareNonVerifiable = errors.New("signature share is not verifiable")
150+
ErrSignShareInvalid = errors.New("signature share is invalid")
151+
)

tss/rsa/signShare_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func TestMarshallFullSignShare(t *testing.T) {
7575
if err != nil {
7676
t.Fatal(err)
7777
}
78-
keys, err := Deal(rand.Reader, players, threshold, key, false)
78+
keys, err := Deal(rand.Reader, players, threshold, key)
7979
if err != nil {
8080
t.Fatal(err)
8181
}

0 commit comments

Comments
 (0)