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 20, 2023
1 parent 24eb204 commit ba95a38
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 53 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.
94 changes: 68 additions & 26 deletions tss/rsa/keyshare.go
Original file line number Diff line number Diff line change
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
}

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
Original file line number Diff line number Diff line change
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
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
Loading

0 comments on commit ba95a38

Please sign in to comment.