From ba95a3821c7d4ca87d3792808162997dc56bd3a5 Mon Sep 17 00:00:00 2001 From: armfazh Date: Tue, 18 Jul 2023 11:41:10 -0700 Subject: [PATCH] Adding verification of signature shares. --- tss/rsa/README.md | 10 ++-- tss/rsa/keyshare.go | 94 +++++++++++++++++++++++++---------- tss/rsa/keyshare_test.go | 4 +- tss/rsa/rsa_threshold.go | 43 ++++++++++++---- tss/rsa/rsa_threshold_test.go | 29 +++++++---- tss/rsa/signShare.go | 48 ++++++++++++++++++ tss/rsa/signShare_test.go | 2 +- 7 files changed, 177 insertions(+), 53 deletions(-) diff --git a/tss/rsa/README.md b/tss/rsa/README.md index b107e3aec..9285d1ca2 100644 --- a/tss/rsa/README.md +++ b/tss/rsa/README.md @@ -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. @@ -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. \ No newline at end of file +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. diff --git a/tss/rsa/keyshare.go b/tss/rsa/keyshare.go index db2f4536a..aa19efda8 100644 --- a/tss/rsa/keyshare.go +++ b/tss/rsa/keyshare.go @@ -10,17 +10,35 @@ 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 - twoDeltaSi *big.Int // optional cached value, this value is used to marginally speed up SignShare generation in Sign. If nil, it will be generated when needed and then cached. + twoDeltaSi *big.Int // this value is used to marginally speed up SignShare generation in Sign. Index uint // When KeyShare's are generated they are each assigned an index sequentially 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 { @@ -51,11 +69,7 @@ func (kshare *KeyShare) MarshalBinary() ([]byte, error) { threshold := uint16(kshare.Threshold) index := uint16(kshare.Index) - twoDeltaSiBytes := []byte(nil) - if kshare.twoDeltaSi != nil { - twoDeltaSiBytes = kshare.twoDeltaSi.Bytes() - } - + twoDeltaSiBytes := kshare.twoDeltaSi.Bytes() twoDeltaSiLen := len(twoDeltaSiBytes) if twoDeltaSiLen > math.MaxInt16 { @@ -86,15 +100,11 @@ func (kshare *KeyShare) MarshalBinary() ([]byte, error) { copy(out[8:8+siLength], siBytes) - if twoDeltaSiBytes != nil { - out[8+siLength] = 1 // twoDeltaSiNil - } + out[8+siLength] = 1 // twoDeltaSiNil binary.BigEndian.PutUint16(out[8+siLength+1:8+siLength+3], uint16(twoDeltaSiLen)) - if twoDeltaSiBytes != nil { - copy(out[8+siLength+3:8+siLength+3+twoDeltaSiLen], twoDeltaSiBytes) - } + copy(out[8+siLength+3:8+siLength+3+twoDeltaSiLen], twoDeltaSiBytes) return out, nil } @@ -132,24 +142,18 @@ func (kshare *KeyShare) UnmarshalBinary(data []byte) error { return fmt.Errorf("rsa_threshold: keyshare unmarshalKeyShareTest failed: data length was too short for reading twoDeltaSiNil") } - isNil := data[8+siLen] - - var twoDeltaSi *big.Int - - if isNil != 0 { - if len(data[8+siLen+1:]) < 2 { - return fmt.Errorf("rsa_threshold: keyshare unmarshalKeyShareTest failed: data length was too short for reading twoDeltaSiLen length") - } - - twoDeltaSiLen := binary.BigEndian.Uint16(data[8+siLen+1 : 8+siLen+3]) + if len(data[8+siLen+1:]) < 2 { + return fmt.Errorf("rsa_threshold: keyshare unmarshalKeyShareTest failed: data length was too short for reading twoDeltaSiLen length") + } - if uint16(len(data[8+siLen+3:])) < twoDeltaSiLen { - return fmt.Errorf("rsa_threshold: keyshare unmarshalKeyShareTest failed: data length was too short for reading twoDeltaSi, needed: %d found: %d", twoDeltaSiLen, len(data[8+siLen+2:])) - } + twoDeltaSiLen := binary.BigEndian.Uint16(data[8+siLen+1 : 8+siLen+3]) - twoDeltaSi = new(big.Int).SetBytes(data[8+siLen+3 : 8+siLen+3+twoDeltaSiLen]) + if uint16(len(data[8+siLen+3:])) < twoDeltaSiLen { + return fmt.Errorf("rsa_threshold: keyshare unmarshalKeyShareTest failed: data length was too short for reading twoDeltaSi, needed: %d found: %d", twoDeltaSiLen, len(data[8+siLen+2:])) } + twoDeltaSi := new(big.Int).SetBytes(data[8+siLen+3 : 8+siLen+3+twoDeltaSiLen]) + kshare.Players = uint(players) kshare.Threshold = uint(threshold) kshare.Index = uint(index) @@ -173,6 +177,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 @@ -248,5 +270,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 } diff --git a/tss/rsa/keyshare_test.go b/tss/rsa/keyshare_test.go index 8005c206d..2775767f7 100644 --- a/tss/rsa/keyshare_test.go +++ b/tss/rsa/keyshare_test.go @@ -117,7 +117,7 @@ func TestMarshallKeyShare(t *testing.T) { marshalTestKeyShare(KeyShare{ si: big.NewInt(10), - twoDeltaSi: nil, + twoDeltaSi: big.NewInt(20), Index: 30, Threshold: 0, Players: 200, @@ -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) } diff --git a/tss/rsa/rsa_threshold.go b/tss/rsa/rsa_threshold.go index 424b8236b..e18a02375 100644 --- a/tss/rsa/rsa_threshold.go +++ b/tss/rsa/rsa_threshold.go @@ -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 { @@ -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) @@ -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 @@ -143,6 +150,14 @@ 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 @@ -150,11 +165,17 @@ func Deal(randSource io.Reader, players, threshold uint, key *rsa.PrivateKey, ca 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), + } } } diff --git a/tss/rsa/rsa_threshold_test.go b/tss/rsa/rsa_threshold_test.go index 82fc224d6..6d85c9954 100644 --- a/tss/rsa/rsa_threshold_test.go +++ b/tss/rsa/rsa_threshold_test.go @@ -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}, @@ -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) @@ -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) } @@ -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) @@ -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) } @@ -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) } @@ -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) } @@ -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) } diff --git a/tss/rsa/signShare.go b/tss/rsa/signShare.go index e73220c12..dfce65f29 100644 --- a/tss/rsa/signShare.go +++ b/tss/rsa/signShare.go @@ -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. @@ -15,6 +19,13 @@ 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 { @@ -22,6 +33,38 @@ func (s SignShare) String() string { 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) { @@ -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") +) diff --git a/tss/rsa/signShare_test.go b/tss/rsa/signShare_test.go index e905d9638..031e4a135 100644 --- a/tss/rsa/signShare_test.go +++ b/tss/rsa/signShare_test.go @@ -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) }