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

tss/rsa: Adding verification of signature shares #453

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -79,6 +79,7 @@ go get -u github.com/cloudflare/circl
#### Zero-knowledge Proofs
- [Schnorr](./zk/dl): Prove knowledge of the Discrete Logarithm.
- [DLEQ](./zk/dleq): Prove knowledge of the Discrete Logarithm Equality.
- [DLEQ in Qn](./zk/qndleq): Prove knowledge of the Discrete Logarithm Equality for subgroup of squares in (Z/nZ)*.

## Testing and Benchmarking

Expand Down
10 changes: 6 additions & 4 deletions tss/rsa/README.md
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.
94 changes: 68 additions & 26 deletions tss/rsa/keyshare.go
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be added to the MarshalBinary/UnmarshalBinary functions https://github.com/cloudflare/circl/blob/tssRSAverif/tss/rsa/keyshare.go#L51

Copy link
Contributor Author

Choose a reason for hiding this comment

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

to ease review, I will propose changes to the serialization functions in a different PR.

}

func (kshare KeyShare) String() string {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
}
4 changes: 2 additions & 2 deletions tss/rsa/keyshare_test.go
Expand Up @@ -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,
Expand Down 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
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 {
armfazh marked this conversation as resolved.
Show resolved Hide resolved
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
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