diff --git a/tss/rsa/README.md b/tss/rsa/README.md new file mode 100644 index 000000000..b107e3aec --- /dev/null +++ b/tss/rsa/README.md @@ -0,0 +1,19 @@ +# RSA Threshold Signatures + +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 + +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. + +Setup consists of a dealer generating *l* key shares from a key pair and "dealing" them to the players. In this implementation the dealer is trusted. + +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 + +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 diff --git a/tss/rsa/internal/pkcs1v15.go b/tss/rsa/internal/pkcs1v15.go new file mode 100644 index 000000000..016b8aa1f --- /dev/null +++ b/tss/rsa/internal/pkcs1v15.go @@ -0,0 +1,110 @@ +// https://cs.opensource.google/go/go/+/refs/tags/go1.18.3:src/crypto/rsa/pkcs1v15.go + +// Copyright (c) 2009 The Go Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package internal + +import ( + "crypto" + "crypto/rsa" + "errors" + "fmt" +) + +var hashPrefixes = map[crypto.Hash][]byte{ + crypto.MD5: { + 0x30, 0x20, 0x30, 0x0c, 0x06, 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x05, 0x05, + 0x00, 0x04, 0x10, + }, + crypto.SHA1: {0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00, 0x04, 0x14}, + crypto.SHA224: { + 0x30, 0x2d, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x04, + 0x05, 0x00, 0x04, 0x1c, + }, + crypto.SHA256: { + 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, + 0x05, 0x00, 0x04, 0x20, + }, + crypto.SHA384: { + 0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, + 0x05, 0x00, 0x04, 0x30, + }, + crypto.SHA512: { + 0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, + 0x05, 0x00, 0x04, 0x40, + }, + crypto.MD5SHA1: {}, // A special TLS case which doesn't use an ASN1 prefix. + crypto.RIPEMD160: {0x30, 0x20, 0x30, 0x08, 0x06, 0x06, 0x28, 0xcf, 0x06, 0x03, 0x00, 0x31, 0x04, 0x14}, +} + +func PadPKCS1v15(pub *rsa.PublicKey, hash crypto.Hash, hashed []byte) ([]byte, error) { + hashLen, prefix, err := pkcs1v15HashInfo(hash, len(hashed)) + if err != nil { + return nil, err + } + + tLen := len(prefix) + hashLen + k := pub.Size() + if k < tLen+11 { + return nil, fmt.Errorf("message too long") + } + + // EM = 0x00 || 0x01 || PS || 0x00 || T + em := make([]byte, k) + em[1] = 1 + for i := 2; i < k-tLen-1; i++ { + em[i] = 0xff + } + copy(em[k-tLen:k-hashLen], prefix) + copy(em[k-hashLen:k], hashed) + + return em, nil +} + +func pkcs1v15HashInfo(hash crypto.Hash, inLen int) (hashLen int, prefix []byte, err error) { + // Special case: crypto.Hash(0) is used to indicate that the data is + // signed directly. + if hash == 0 { + return inLen, nil, nil + } + + hashLen = hash.Size() + if inLen != hashLen { + return 0, nil, errors.New("threshold_internal: crypto/rsa: input must be hashed message") + } + prefix, ok := hashPrefixes[hash] + if !ok { + return 0, nil, errors.New("threshold_internal: crypto/rsa: unsupported hash function") + } + return +} diff --git a/tss/rsa/internal/pss/pss.go b/tss/rsa/internal/pss/pss.go new file mode 100644 index 000000000..01ce80b5a --- /dev/null +++ b/tss/rsa/internal/pss/pss.go @@ -0,0 +1,169 @@ +// https://cs.opensource.google/go/go/+/refs/tags/go1.18.3:src/crypto/rsa/pss.go + +// Copyright (c) 2009 The Go Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package pss + +// This file implements the RSASSA-PSS signature scheme according to RFC 8017. + +import ( + "crypto" + "crypto/rsa" + "errors" + "hash" + "io" +) + +// Per RFC 8017, Section 9.1 +// +// EM = MGF1 xor DB || H( 8*0x00 || mHash || salt ) || 0xbc +// +// where +// +// DB = PS || 0x01 || salt +// +// and PS can be empty so +// +// emLen = dbLen + hLen + 1 = psLen + sLen + hLen + 2 +// + +func emsaPSSEncode(mHash []byte, emBits int, salt []byte, hash hash.Hash) ([]byte, error) { + // See RFC 8017, Section 9.1.1. + + hLen := hash.Size() + sLen := len(salt) + emLen := (emBits + 7) / 8 + + // 1. If the length of M is greater than the input limitation for the + // hash function (2^61 - 1 octets for SHA-1), output "message too + // long" and stop. + // + // 2. Let mHash = Hash(M), an octet string of length hLen. + + if len(mHash) != hLen { + return nil, errors.New("crypto/rsa: input must be hashed with given hash") + } + + // 3. If emLen < hLen + sLen + 2, output "encoding error" and stop. + + if emLen < hLen+sLen+2 { + return nil, errors.New("crypto/rsa: key size too small for PSS signature") + } + + em := make([]byte, emLen) + psLen := emLen - sLen - hLen - 2 + db := em[:psLen+1+sLen] + h := em[psLen+1+sLen : emLen-1] + + // 4. Generate a random octet string salt of length sLen; if sLen = 0, + // then salt is the empty string. + // + // 5. Let + // M' = (0x)00 00 00 00 00 00 00 00 || mHash || salt; + // + // M' is an octet string of length 8 + hLen + sLen with eight + // initial zero octets. + // + // 6. Let H = Hash(M'), an octet string of length hLen. + + var prefix [8]byte + + hash.Write(prefix[:]) + hash.Write(mHash) + hash.Write(salt) + + h = hash.Sum(h[:0]) + hash.Reset() + + // 7. Generate an octet string PS consisting of emLen - sLen - hLen - 2 + // zero octets. The length of PS may be 0. + // + // 8. Let DB = PS || 0x01 || salt; DB is an octet string of length + // emLen - hLen - 1. + + db[psLen] = 0x01 + copy(db[psLen+1:], salt) + + // 9. Let dbMask = MGF(H, emLen - hLen - 1). + // + // 10. Let maskedDB = DB \xor dbMask. + + mgf1XOR(db, hash, h) + + // 11. Set the leftmost 8 * emLen - emBits bits of the leftmost octet in + // maskedDB to zero. + + db[0] &= 0xff >> (8*emLen - emBits) + + // 12. Let EM = maskedDB || H || 0xbc. + em[emLen-1] = 0xbc + + // 13. Output EM. + return em, nil +} + +func padPSSWithSalt(pub *rsa.PublicKey, hash crypto.Hash, hashed, salt []byte) ([]byte, error) { + emBits := pub.N.BitLen() - 1 + em, err := emsaPSSEncode(hashed, emBits, salt, hash.New()) + if err != nil { + return nil, err + } + return em, nil +} + +func saltLength(opts *rsa.PSSOptions) int { + if opts == nil { + return rsa.PSSSaltLengthAuto + } + return opts.SaltLength +} + +func PadPSS(rand io.Reader, pub *rsa.PublicKey, hash crypto.Hash, digest []byte, opts *rsa.PSSOptions) ([]byte, error) { + if opts != nil && opts.Hash != 0 { + hash = opts.Hash + } + + saltLength := saltLength(opts) + switch saltLength { + case rsa.PSSSaltLengthAuto: + saltLength = (pub.N.BitLen()-1+7)/8 - 2 - hash.Size() + case rsa.PSSSaltLengthEqualsHash: + saltLength = hash.Size() + } + + salt := make([]byte, saltLength) + if _, err := io.ReadFull(rand, salt); err != nil { + return nil, err + } + return padPSSWithSalt(pub, hash, digest, salt) +} diff --git a/tss/rsa/internal/pss/rsa.go b/tss/rsa/internal/pss/rsa.go new file mode 100644 index 000000000..0d7282206 --- /dev/null +++ b/tss/rsa/internal/pss/rsa.go @@ -0,0 +1,70 @@ +// https://cs.opensource.google/go/go/+/refs/tags/go1.18.3:src/crypto/rsa/rsa.go + +// Copyright (c) 2009 The Go Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package pss + +import ( + "hash" +) + +// incCounter increments a four byte, big-endian counter. +func incCounter(c *[4]byte) { + if c[3]++; c[3] != 0 { + return + } + if c[2]++; c[2] != 0 { + return + } + if c[1]++; c[1] != 0 { + return + } + c[0]++ +} + +// mgf1XOR XORs the bytes in out with a mask generated using the MGF1 function +// specified in PKCS #1 v2.1. +func mgf1XOR(out []byte, hash hash.Hash, seed []byte) { + var counter [4]byte + var digest []byte + + done := 0 + for done < len(out) { + hash.Write(seed) + hash.Write(counter[0:4]) + digest = hash.Sum(digest[:0]) + hash.Reset() + + for i := 0; i < len(digest) && done < len(out); i++ { + out[done] ^= digest[i] + done++ + } + incCounter(&counter) + } +} diff --git a/tss/rsa/keyshare.go b/tss/rsa/keyshare.go new file mode 100644 index 000000000..8abc786a6 --- /dev/null +++ b/tss/rsa/keyshare.go @@ -0,0 +1,247 @@ +package rsa + +import ( + "crypto/rand" + "crypto/rsa" + "encoding/binary" + "errors" + "fmt" + "io" + "math" + "math/big" + "sync" +) + +// 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. + Index uint // When KeyShare's are generated they are each assigned an index sequentially + + Players uint + Threshold uint +} + +// MarshalBinary encodes a KeyShare into a byte array in a format readable by UnmarshalBinary. +// Note: Only Index's up to math.MaxUint16 are supported +func (kshare *KeyShare) MarshalBinary() ([]byte, error) { + // The encoding format is + // | Players: uint16 | Threshold: uint16 | Index: uint16 | siLen: uint16 | si: []byte | twoDeltaSiNil: bool | twoDeltaSiLen: uint16 | twoDeltaSi: []byte | + // with all values in big-endian. + + if kshare.Players > math.MaxUint16 { + return nil, fmt.Errorf("rsa_threshold: keyshare marshall: Players is too big to fit in a uint16") + } + + if kshare.Threshold > math.MaxUint16 { + return nil, fmt.Errorf("rsa_threshold: keyshare marshall: Threhsold is too big to fit in a uint16") + } + + if kshare.Index > math.MaxUint16 { + return nil, fmt.Errorf("rsa_threshold: keyshare marshall: Index is too big to fit in a uint16") + } + + players := uint16(kshare.Players) + threshold := uint16(kshare.Threshold) + index := uint16(kshare.Index) + + twoDeltaSiBytes := []byte(nil) + if kshare.twoDeltaSi != nil { + twoDeltaSiBytes = kshare.twoDeltaSi.Bytes() + } + + twoDeltaSiLen := len(twoDeltaSiBytes) + + if twoDeltaSiLen > math.MaxInt16 { + return nil, fmt.Errorf("rsa_threshold: keyshare marshall: twoDeltaSiBytes is too big to fit it's length in a uint16") + } + + siBytes := kshare.si.Bytes() + + siLength := len(siBytes) + + if siLength == 0 { + siLength = 1 + siBytes = []byte{0} + } + + if siLength > math.MaxInt16 { + return nil, fmt.Errorf("rsa_threshold: keyshare marshall: siBytes is too big to fit it's length in a uint16") + } + + blen := 2 + 2 + 2 + 2 + 2 + 1 + siLength + twoDeltaSiLen + out := make([]byte, blen) + + binary.BigEndian.PutUint16(out[0:2], players) + binary.BigEndian.PutUint16(out[2:4], threshold) + binary.BigEndian.PutUint16(out[4:6], index) + + binary.BigEndian.PutUint16(out[6:8], uint16(siLength)) // okay because of conditions checked above + + copy(out[8:8+siLength], siBytes) + + if twoDeltaSiBytes != nil { + 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) + } + + return out, nil +} + +// UnmarshalBinary recovers a KeyShare from a slice of bytes, or returns an error if the encoding is invalid. +func (kshare *KeyShare) UnmarshalBinary(data []byte) error { + // The encoding format is + // | Players: uint16 | Threshold: uint16 | Index: uint16 | siLen: uint16 | si: []byte | twoDeltaSiNil: bool | twoDeltaSiLen: uint16 | twoDeltaSi: []byte | + // with all values in big-endian. + if len(data) < 6 { + return fmt.Errorf("rsa_threshold: keyshare unmarshalKeyShareTest failed: data length was too short for reading Players, Threashold, Index") + } + + players := binary.BigEndian.Uint16(data[0:2]) + threshold := binary.BigEndian.Uint16(data[2:4]) + index := binary.BigEndian.Uint16(data[4:6]) + + if len(data[6:]) < 2 { + return fmt.Errorf("rsa_threshold: keyshare unmarshalKeyShareTest failed: data length was too short for reading siLen length") + } + + siLen := binary.BigEndian.Uint16(data[6:8]) + + if siLen == 0 { + return fmt.Errorf("rsa_threshold: keyshare unmarshalKeyShareTest failed: si is a required field but siLen was 0") + } + + if uint16(len(data[8:])) < siLen { + return fmt.Errorf("rsa_threshold: keyshare unmarshalKeyShareTest failed: data length was too short for reading si, needed: %d found: %d", siLen, len(data[8:])) + } + + si := new(big.Int).SetBytes(data[8 : 8+siLen]) + + if len(data[8+siLen:]) < 1 { + 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 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) + kshare.si = si + kshare.twoDeltaSi = twoDeltaSi + + return nil +} + +// Returns the cached value in twoDeltaSi or if nil, generates 2∆s_i, stores it in twoDeltaSi, and returns it +func (kshare *KeyShare) get2DeltaSi(players int64) *big.Int { + // use the cached value if it exists + if kshare.twoDeltaSi != nil { + return kshare.twoDeltaSi + } + delta := calculateDelta(players) + // 2∆s_i + // delta << 1 == delta * 2 + delta.Lsh(delta, 1).Mul(delta, kshare.si) + kshare.twoDeltaSi = delta + return delta +} + +// 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 +// side-channel attacks. +// +// parallel indicates whether the blinding operations should use go routines to operate in parallel. +// If parallel is false, blinding will take about 2x longer than nonbinding, otherwise it will take about the same time +// (see benchmarks). If randSource is nil, parallel has no effect. parallel should almost always be set to true. +func (kshare *KeyShare) Sign(randSource io.Reader, pub *rsa.PublicKey, digest []byte, parallel bool) (SignShare, error) { + x := &big.Int{} + x.SetBytes(digest) + + exp := kshare.get2DeltaSi(int64(kshare.Players)) + + var signShare SignShare + signShare.Players = kshare.Players + signShare.Threshold = kshare.Threshold + signShare.Index = kshare.Index + + signShare.xi = &big.Int{} + + if randSource != nil { + // Let's blind. + // We can't use traditional RSA blinding (as used in rsa.go) because we are exponentiating by exp and not d. + // As such, Euler's theorem doesn't apply ( exp * d != 0 (mod ϕ(n)) ). + // Instead, we will choose a random r and compute x^{exp+r} * x^{-r} = x^{exp}. + // This should (hopefully) prevent revealing information of the true value of exp, since with exp you can derive + // s_i, the secret key share. + + r, err := rand.Int(randSource, pub.N) + if err != nil { + return SignShare{}, errors.New("rsa_threshold: unable to get random value for blinding") + } + expPlusr := big.Int{} + // exp + r + expPlusr.Add(exp, r) + + var wg *sync.WaitGroup + + // x^{|2∆s_i+r|} + if parallel { + wg = &sync.WaitGroup{} + wg.Add(1) + go func() { + signShare.xi.Exp(x, &expPlusr, pub.N) + wg.Done() + }() + } else { + signShare.xi.Exp(x, &expPlusr, pub.N) + } + + xExpr := big.Int{} + // x^r + xExpr.Exp(x, r, pub.N) + // x^{-r} + res := xExpr.ModInverse(&xExpr, pub.N) + + if res == nil { + // extremely unlikely, somehow x^r is p or q + return SignShare{}, errors.New("rsa_threshold: no mod inverse") + } + + if wg != nil { + wg.Wait() + } + + // x^{|2∆s_i+r|} * x^{-r} = x^{2∆s_i} + signShare.xi.Mul(signShare.xi, &xExpr) + signShare.xi.Mod(signShare.xi, pub.N) + } else { + // x^{2∆s_i} + signShare.xi = &big.Int{} + signShare.xi.Exp(x, exp, pub.N) + } + + return signShare, nil +} diff --git a/tss/rsa/keyshare_test.go b/tss/rsa/keyshare_test.go new file mode 100644 index 000000000..8005c206d --- /dev/null +++ b/tss/rsa/keyshare_test.go @@ -0,0 +1,161 @@ +package rsa + +import ( + "crypto/rand" + "crypto/rsa" + "math/big" + "testing" +) + +func TestKeyShare_Sign(t *testing.T) { + // delta = 3! = 6 + // n = 253 + // Players = 3 + // kshare = { si: 15, Index: 1 } + // x = { 150 } + // x_i = x^{2∆kshare.si} = 150^{2 * 6 * 15} = 150^180 = 243 + + kshare := KeyShare{ + si: big.NewInt(15), + Index: 1, + Players: 3, + } + pub := rsa.PublicKey{N: big.NewInt(253)} + share, err := kshare.Sign(nil, &pub, []byte{150}, false) + if err != nil { + t.Fatal(err) + } + if share.xi.Cmp(big.NewInt(243)) != 0 { + t.Fatalf("share.xi should be 243 but was %d", share.xi) + } +} + +func testSignBlind(parallel bool, t *testing.T) { + // delta = 3! = 6 + // n = 253 + // Players = 3 + // kshare = { si: 15, i: 1 } + // x = { 150 } + // x_i = x^{2∆kshare.si} = 150^{2 * 6 * 15} = 150^180 = 243 + + kshare := KeyShare{ + si: big.NewInt(15), + Index: 1, + Players: 3, + } + pub := rsa.PublicKey{N: big.NewInt(253)} + share, err := kshare.Sign(rand.Reader, &pub, []byte{150}, parallel) + if err != nil { + t.Fatal(err) + } + if share.xi.Cmp(big.NewInt(243)) != 0 { + t.Fatalf("share.xi should be 243 but was %d", share.xi) + } +} + +func TestKeyShare_SignBlind(t *testing.T) { + testSignBlind(false, t) +} + +func TestKeyShare_SignBlindParallel(t *testing.T) { + testSignBlind(true, t) +} + +func marshalTestKeyShare(share KeyShare, t *testing.T) { + marshall, err := share.MarshalBinary() + if err != nil { + t.Fatal(err) + } + + share2 := KeyShare{} + err = share2.UnmarshalBinary(marshall) + if err != nil { + t.Fatal(err) + } + + if share.Players != share2.Players { + t.Fatalf("Players did not match, expected %d, found %d", share.Players, share2.Players) + } + + if share.Threshold != share2.Threshold { + t.Fatalf("Threshold did not match, expected %d, found %d", share.Threshold, share2.Threshold) + } + + if share.Index != share2.Index { + t.Fatalf("Index did not match, expected %d, found %d", share.Index, share2.Index) + } + + if (share.twoDeltaSi == nil || share2.twoDeltaSi == nil) && share.twoDeltaSi != share2.twoDeltaSi { + t.Fatalf("twoDeltaSi did not match, expected %v, found %v", share.twoDeltaSi, share2.twoDeltaSi) + } + + if !(share.twoDeltaSi == nil && share2.twoDeltaSi == nil) && share.twoDeltaSi.Cmp(share2.twoDeltaSi) != 0 { + t.Fatalf("twoDeltaSi did not match, expected %v, found %v", share.twoDeltaSi.Bytes(), share2.twoDeltaSi.Bytes()) + } + + if share.si.Cmp(share2.si) != 0 { + t.Fatalf("si did not match, expected %v, found %v", share.si.Bytes(), share2.si.Bytes()) + } +} + +func unmarshalKeyShareTest(t *testing.T, input []byte) { + share := KeyShare{} + err := share.UnmarshalBinary(input) + if err == nil { + t.Fatalf("unmarshall succeeded when it shouldn't have") + } +} + +func TestMarshallKeyShare(t *testing.T) { + marshalTestKeyShare(KeyShare{ + si: big.NewInt(10), + twoDeltaSi: big.NewInt(20), + Index: 30, + Threshold: 10, + Players: 2, + }, t) + + marshalTestKeyShare(KeyShare{ + si: big.NewInt(10), + twoDeltaSi: nil, + Index: 30, + Threshold: 0, + Players: 200, + }, t) + + marshalTestKeyShare(KeyShare{ + si: big.NewInt(0), + twoDeltaSi: big.NewInt(0), + Index: 0, + Threshold: 0, + Players: 0, + }, t) + + unmarshalKeyShareTest(t, []byte{}) + unmarshalKeyShareTest(t, []byte{1, 0, 1}) + unmarshalKeyShareTest(t, []byte{1, 0, 1}) + unmarshalKeyShareTest(t, []byte{0, 1, 0, 1, 0, 1}) + unmarshalKeyShareTest(t, []byte{0, 1, 0, 1, 0, 1, 0, 1}) + unmarshalKeyShareTest(t, []byte{0, 1, 0, 1, 0, 1, 0}) + unmarshalKeyShareTest(t, []byte{0, 1, 0, 1, 0, 1, 0, 2, 1}) + unmarshalKeyShareTest(t, []byte{0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0}) + unmarshalKeyShareTest(t, []byte{0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1}) +} + +func TestMarshallKeyShareFull(t *testing.T) { + const players = 3 + const threshold = 2 + const bits = 4096 + + key, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + t.Fatal(err) + } + keys, err := Deal(rand.Reader, players, threshold, key, false) + if err != nil { + t.Fatal(err) + } + for _, share := range keys { + marshalTestKeyShare(share, t) + } +} diff --git a/tss/rsa/padding.go b/tss/rsa/padding.go new file mode 100644 index 000000000..e251ba156 --- /dev/null +++ b/tss/rsa/padding.go @@ -0,0 +1,38 @@ +package rsa + +import ( + "crypto" + "crypto/rsa" + "io" + + "github.com/cloudflare/circl/tss/rsa/internal" + pss2 "github.com/cloudflare/circl/tss/rsa/internal/pss" +) + +type Padder interface { + Pad(pub *rsa.PublicKey, hash crypto.Hash, hashed []byte) ([]byte, error) +} + +type PKCS1v15Padder struct{} + +func (PKCS1v15Padder) Pad(pub *rsa.PublicKey, hash crypto.Hash, hashed []byte) ([]byte, error) { + return internal.PadPKCS1v15(pub, hash, hashed) +} + +// PSSPadder is a padder for RSA Probabilistic Padding Scheme (RSA-PSS) used in TLS 1.3 +// +// Note: If the salt length is non-zero, PSS padding is not deterministic. +// TLS 1.3 mandates that the salt length is the same as the hash output length. As such, each player cannot +// pad the message individually, otherwise they will produce unique messages and the signature will not be valid. +// Instead, one party should generate a random saltLen byte string. When requesting signatures from the rest of the +// parties they should send along the same random string to be used as `rand` here. +// +// For TLS, rsa.PSSOptions.SaltLength should be PSSSaltLengthEqualsHash. +type PSSPadder struct { + Rand io.Reader + Opts *rsa.PSSOptions +} + +func (pss *PSSPadder) Pad(pub *rsa.PublicKey, hash crypto.Hash, hashed []byte) ([]byte, error) { + return pss2.PadPSS(pss.Rand, pub, hash, hashed, pss.Opts) +} diff --git a/tss/rsa/rsa_threshold.go b/tss/rsa/rsa_threshold.go new file mode 100644 index 000000000..e82af4c49 --- /dev/null +++ b/tss/rsa/rsa_threshold.go @@ -0,0 +1,271 @@ +package rsa + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "errors" + "fmt" + "io" + "math" + "math/big" +) + +// l or `Players`, the total number of Players. +// t, the number of corrupted Players. +// k=t+1 or `Threshold`, the number of signature shares needed to obtain a signature. + +func validateParams(players, threshold uint) error { + if players <= 1 { + return errors.New("rsa_threshold: Players (l) invalid: should be > 1") + } + if threshold < 1 || threshold > players { + return fmt.Errorf("rsa_threshold: Threshold (k) invalid: %d < 1 || %d > %d", threshold, threshold, players) + } + 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) { + err := validateParams(players, threshold) + + ONE := big.NewInt(1) + + if err != nil { + return nil, err + } + + if len(key.Primes) != 2 { + return nil, errors.New("multiprime rsa keys are unsupported") + } + + p := key.Primes[0] + q := key.Primes[1] + e := int64(key.E) + + // p = 2p' + 1 + // q = 2q' + 1 + // p' = (p - 1)/2 + // q' = (q - 1)/2 + // m = p'q' = (p - 1)(q - 1)/4 + + var pprime big.Int + // p - 1 + pprime.Sub(p, ONE) + + // q - 1 + var m big.Int + m.Sub(q, ONE) + // (p - 1)(q - 1) + m.Mul(&m, &pprime) + // >> 2 == / 4 + m.Rsh(&m, 2) + + // de ≡ 1 + var d big.Int + _d := d.ModInverse(big.NewInt(e), &m) + + if _d == nil { + return nil, errors.New("rsa_threshold: no ModInverse for e in Z/Zm") + } + + // a_0...a_{k-1} + a := make([]*big.Int, threshold) + // a_0 = d + a[0] = &d + + // a_0...a_{k-1} = rand from {0, ..., m - 1} + for i := uint(1); i <= threshold-1; i++ { + a[i], err = rand.Int(randSource, &m) + if err != nil { + return nil, errors.New("rsa_threshold: unable to generate an int within [0, m)") + } + } + + 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 + shares[i-1].Index = i + if cache { + shares[i-1].get2DeltaSi(int64(players)) + } + } + + return shares, nil +} + +func calcN(p, q *big.Int) big.Int { + // n = pq + var n big.Int + n.Mul(p, q) + return n +} + +// f(X) = Σ^{k-1}_{i=0} | a_i * X^i (mod m) +func computePolynomial(k uint, a []*big.Int, x uint, m *big.Int) *big.Int { + // TODO: use Horner's method here. + sum := big.NewInt(0) + // Σ^{k-1}_{i=0} + for i := uint(0); i <= k-1; i++ { + // X^i + // TODO optimize: we can compute x^{n+1} from the previous x^n + xi := int64(math.Pow(float64(x), float64(i))) + // a_i * X^i + prod := big.Int{} + prod.Mul(a[i], big.NewInt(xi)) + // (mod m) + prod.Mod(&prod, m) // while not in the spec, we are eventually modding m, so we can mod here for efficiency + // Σ + sum.Add(sum, &prod) + } + + sum.Mod(sum, m) + + return sum +} + +// PadHash MUST be called before signing a message +func PadHash(padder Padder, hash crypto.Hash, pub *rsa.PublicKey, msg []byte) ([]byte, error) { + // Sign(Pad(Hash(M))) + + hasher := hash.New() + hasher.Write(msg) + digest := hasher.Sum(nil) + + return padder.Pad(pub, hash, digest) +} + +type Signature = []byte + +// CombineSignShares combines t SignShare's to produce a valid signature +func CombineSignShares(pub *rsa.PublicKey, shares []SignShare, msg []byte) (Signature, error) { + players := shares[0].Players + threshold := shares[0].Threshold + + for i := range shares { + if shares[i].Players != players { + return nil, errors.New("rsa_threshold: shares didn't have consistent players") + } + if shares[i].Threshold != threshold { + return nil, errors.New("rsa_threshold: shares didn't have consistent threshold") + } + } + + if uint(len(shares)) < threshold { + return nil, errors.New("rsa_threshold: insufficient shares for the threshold") + } + + w := big.NewInt(1) + delta := calculateDelta(int64(players)) + // i_1 ... i_k + for _, share := range shares { + // λ(S, 0, i) + lambda, err := computeLambda(delta, shares, 0, int64(share.Index)) + if err != nil { + return nil, err + } + // 2λ + var exp big.Int + exp.Add(lambda, lambda) // faster than TWO * lambda + + // we need to handle negative λ's (aka inverse), so abs it, compare, and if necessary modinverse + abslam := big.Int{} + abslam.Abs(&exp) + var tmp big.Int + // x_i^{|2λ|} + tmp.Exp(share.xi, &abslam, pub.N) + if abslam.Cmp(&exp) == 1 { + tmp.ModInverse(&tmp, pub.N) + } + // TODO first compute all the powers for the negative exponents (but don't invert yet); multiply these together and then invert all at once. This is ok since (ab)^-1 = a^-1 b^-1 + + w.Mul(w, &tmp).Mod(w, pub.N) + } + w.Mod(w, pub.N) + + // e′ = 4∆^2 + eprime := big.Int{} + eprime.Mul(delta, delta) // faster than delta^TWO + eprime.Add(&eprime, &eprime) // faster than FOUR * eprime + eprime.Add(&eprime, &eprime) + + // e′a + eb = 1 + a := big.Int{} + b := big.Int{} + tmp := big.Int{} + tmp.GCD(&a, &b, &eprime, big.NewInt(int64(pub.E))) + + // TODO You can compute a earlier and multiply a into the exponents used when computing w. + // w^a + wa := big.Int{} + wa.Exp(w, &a, pub.N) // TODO justification + // x^b + xb := big.Int{} + xb.SetBytes(msg) + xb.Exp(&xb, &b, pub.N) // TODO justification + // y = w^a * x^b + y := big.Int{} + y.Mul(&wa, &xb).Mod(&y, pub.N) + + return y.Bytes(), nil +} + +// computes lagrange Interpolation for the shares +// i must be an id 0..l but not in S +// j must be in S +func computeLambda(delta *big.Int, S []SignShare, i, j int64) (*big.Int, error) { + if i == j { + return nil, errors.New("rsa_threshold: i and j can't be equal by precondition") + } + // these are just to check preconditions + foundi := false + foundj := false + + // λ(s, i, j) = ∆( ( π{j'∈S\{j}} (i - j') ) / ( π{j'∈S\{j}} (j - j') ) ) + + num := int64(1) + den := int64(1) + + // ∈ S + for _, s := range S { + // j' + jprime := int64(s.Index) + // S\{j} + if jprime == j { + foundj = true + continue + } + if jprime == i { + foundi = false + break + } + // (i - j') + num *= i - jprime + // (j - j') + den *= j - jprime + } + + // ∆ * (num/den) + var lambda big.Int + // (num/den) + lambda.Div(big.NewInt(num), big.NewInt(den)) + // ∆ * (num/den) + lambda.Mul(delta, &lambda) + + if foundi { + return nil, fmt.Errorf("rsa_threshold: i: %d should not be in S", i) + } + + if !foundj { + return nil, fmt.Errorf("rsa_threshold: j: %d should be in S", j) + } + + return &lambda, nil +} diff --git a/tss/rsa/rsa_threshold_test.go b/tss/rsa/rsa_threshold_test.go new file mode 100644 index 000000000..da5e549ae --- /dev/null +++ b/tss/rsa/rsa_threshold_test.go @@ -0,0 +1,421 @@ +package rsa + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + _ "crypto/sha256" + "errors" + "io" + "math/big" + "testing" +) + +var ONE = big.NewInt(1) + +func createPrivateKey(p, q *big.Int, e int) *rsa.PrivateKey { + return &rsa.PrivateKey{ + PublicKey: rsa.PublicKey{ + E: e, + }, + D: nil, + Primes: []*big.Int{p, q}, + Precomputed: rsa.PrecomputedValues{}, + } +} + +func TestCalcN(t *testing.T) { + TWO := big.NewInt(2) + n := calcN(ONE, TWO) + if n.Cmp(TWO) != 0 { + t.Fatal("calcN failed: (1, 2)") + } + n = calcN(TWO, big.NewInt(4)) + if n.Cmp(big.NewInt(8)) != 0 { + t.Fatal("calcN failed: (2, 4)") + } +} + +func TestComputePolynomial(t *testing.T) { + m := big.NewInt(11) + const k = 5 + a := make([]*big.Int, k) + for i := 0; i < k; i++ { + a[i] = big.NewInt(int64(i + 1)) + } + // a = {1, 2, 3, 4, 5} + + x := uint(3) + out := computePolynomial(k, a, x, m) + // 1 * 3^0 = 1 = 1 + // 2 * 3^1 = 6 = 6 + // 3 * 3^2 = 27 = 5 + // 4 * 3^3 = 108 = 9 + // 5 * 3^4 = 405 = 9 + // 1 + 6 + 5 + 9 + 9 = 30 = 8 + if out.Cmp(big.NewInt(8)) != 0 { + t.Fatal("compute polynomial failed") + } +} + +func TestComputeLambda(t *testing.T) { + // shares = {1, 2, 3, 4, 5} + // i = 0 + // ∆ = 5! = 120 + // j = 3 + // + // num = (0 - 1) * (0 - 2) * (0 - 4) * (0 - 5) = 40 + // dem = (3 - 1) * (3 - 2) * (3 - 4) * (3 - 5) = 4 + // num/dev = 40/4 = 10 + // ∆ * 10 = 120 * 10 = 1200 + shares := make([]SignShare, 5) + for i := uint(1); i <= 5; i++ { + shares[i-1].Index = i + } + i := int64(0) + delta := int64(120) + j := int64(3) + + lambda, err := computeLambda(big.NewInt(delta), shares, i, j) + + if err != nil || lambda.Cmp(big.NewInt(1200)) != 0 { + t.Fatal("computeLambda failed") + } +} + +func TestDeal(t *testing.T) { + // Players = 3 + // Threshold = 2 + // e = 3 + // p' = 11 + // q' = 5 + // p = 2(11) + 1 = 23 + // q = 2(5) + 1 = 11 + // n = 253 + // m = 55 + // d = 37 + // + // a[0] = 37 + // a[1] = 33 + // + // + // Index = 1 + // computePolynomial(k: 2, a: {37, 33}, x: 1, m: 55) : + // 37 * 1^0 = 37 * 1 = 37 + // 33 * 1^1 = 33 * 1 = 33 + // 37 + 33 = 70 = 15 + // + // shares[0].si = 15 + // shares[0].Index = 1 + // + // Index = 2 + // computePolynomial(k: 2, a: {37, 33}, x: 2, m: 55) : + // 37 * 2^0 = 37 * 1 = 37 + // 33 * 2^1 = 33 * 2 = 66 = 11 + // 37 + 11 = 48 + // + // shares[1].si = 48 + // shares[1].Index = 2 + // + // + // Index = 3 + // computePolynomial(k: 2, a: {37, 33}, x: 3, m: 55) : + // 37 * 3^0 = 37 * 1 = 37 + // 33 * 3^1 = 33 * 3 = 99 = 44 + // 37 + 44 = 81 = 26 + // + // shares[2].si = 26 + // shares[2].Index = 3 + // + // + // + r := bytes.NewReader([]byte{33, 17}) + players := uint(3) + threshold := uint(2) + p := int64(23) + q := int64(11) + e := 3 + + key := createPrivateKey(big.NewInt(p), big.NewInt(q), e) + + share, err := Deal(r, players, threshold, key, false) + if err != nil { + t.Fatal(err) + } + if share[0].si.Cmp(big.NewInt(15)) != 0 { + t.Fatalf("share[0].si should have been 15 but was %d", share[0].si) + } + if share[1].si.Cmp(big.NewInt(48)) != 0 { + t.Fatalf("share[1].si should have been 48 but was %d", share[1].si) + } + if share[2].si.Cmp(big.NewInt(26)) != 0 { + t.Fatalf("share[2].si should have been 26 but was %d", share[2].si) + } +} + +const ( + PKS1v15 = 0 + PSS = 1 +) + +func testIntegration(t *testing.T, algo crypto.Hash, pub *rsa.PublicKey, threshold uint, keys []KeyShare, padScheme int) { + t.Logf("dealt %d keys", len(keys)) + + msg := []byte("hello") + + var padder Padder + if padScheme == PKS1v15 { + padder = &PKCS1v15Padder{} + } else if padScheme == PSS { + padder = &PSSPadder{ + Rand: rand.Reader, + Opts: nil, + } + } else { + t.Fatal(errors.New("unknown padScheme")) + } + + msgPH, err := PadHash(padder, algo, pub, msg) + if err != nil { + t.Fatal(err) + } + + signshares := make([]SignShare, threshold) + + for i := uint(0); i < threshold; i++ { + signshares[i], err = keys[i].Sign(rand.Reader, pub, msgPH, true) + if err != nil { + t.Fatal(err) + } + } + + t.Logf("signed with %d keys", len(signshares)) + + sig, err := CombineSignShares(pub, signshares, msgPH) + if err != nil { + t.Fatal(err) + } + + t.Log("combined sign shares") + + h := algo.New() + h.Write(msg) + hashed := h.Sum(nil) + + if padScheme == PKS1v15 { + err = rsa.VerifyPKCS1v15(pub, algo, hashed, sig) + } else if padScheme == PSS { + err = rsa.VerifyPSS(pub, algo, hashed, sig, padder.(*PSSPadder).Opts) + } else { + panic("logical error") + } + + if err != nil { + t.Fatal(err) + } +} + +func TestIntegrationStdRsaKeyGenerationPKS1v15(t *testing.T) { + const players = 3 + const threshold = 2 + const bits = 2048 + const algo = crypto.SHA256 + + key, err := rsa.GenerateKey(rand.Reader, bits) + pub := key.PublicKey + if err != nil { + t.Fatal(err) + } + keys, err := Deal(rand.Reader, players, threshold, key, false) + if err != nil { + t.Fatal(err) + } + testIntegration(t, algo, &pub, threshold, keys, PKS1v15) +} + +func TestIntegrationStdRsaKeyGenerationPSS(t *testing.T) { + const players = 3 + const threshold = 2 + const bits = 2048 + const algo = crypto.SHA256 + + key, err := rsa.GenerateKey(rand.Reader, bits) + pub := key.PublicKey + if err != nil { + t.Fatal(err) + } + keys, err := Deal(rand.Reader, players, threshold, key, false) + if err != nil { + t.Fatal(err) + } + testIntegration(t, algo, &pub, threshold, keys, PSS) +} + +// nolint: unparam +func benchmarkSignCombineHelper(randSource io.Reader, parallel bool, b *testing.B, players, threshold uint, bits int, algo crypto.Hash, padScheme int) { + key, err := rsa.GenerateKey(rand.Reader, bits) + pub := key.PublicKey + if err != nil { + panic(err) + } + + keys, err := Deal(rand.Reader, players, threshold, key, true) + if err != nil { + b.Fatal(err) + } + + msg := []byte("hello") + var padder Padder + if padScheme == PKS1v15 { + padder = &PKCS1v15Padder{} + } else if padScheme == PSS { + padder = &PSSPadder{ + Rand: rand.Reader, + Opts: nil, + } + } else { + b.Fatal(errors.New("unknown padScheme")) + } + msgPH, err := PadHash(padder, algo, &pub, msg) + if err != nil { + b.Fatal(err) + } + + signshares := make([]SignShare, threshold) + b.ResetTimer() + for i := 0; i < b.N; i++ { + for i := uint(0); i < threshold; i++ { + signshares[i], err = keys[i].Sign(randSource, &pub, msgPH, parallel) + if err != nil { + b.Fatal(err) + } + } + _, err = CombineSignShares(&pub, signshares, msgPH) + if err != nil { + b.Fatal(err) + } + } + b.StopTimer() +} + +func BenchmarkBaselineRSA_SHA256_4096(b *testing.B) { + const bits = 4096 + const algo = crypto.SHA256 + + key, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + b.Fatal(err) + } + h := algo.New() + + msg := []byte("hello") + + h.Write(msg) + d := h.Sum(nil) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err = rsa.SignPKCS1v15(rand.Reader, key, algo, d) + if err != nil { + b.Fatal(err) + } + } + b.StopTimer() +} + +func BenchmarkBaselineRSA_SHA256_2048(b *testing.B) { + const bits = 2048 + const algo = crypto.SHA256 + + key, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + b.Fatal(err) + } + h := algo.New() + + msg := []byte("hello") + + h.Write(msg) + d := h.Sum(nil) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err = rsa.SignPKCS1v15(rand.Reader, key, algo, d) + if err != nil { + b.Fatal(err) + } + } + b.StopTimer() +} + +func BenchmarkSignCombine_SHA256_4096_3_2_Scheme(b *testing.B) { + const players = 3 + const threshold = 2 + const bits = 4096 + const algo = crypto.SHA256 + benchmarkSignCombineHelper(nil, false, b, players, threshold, bits, algo, PKS1v15) +} + +func BenchmarkSignCombine_SHA256_4096_3_2_Scheme_Blind(b *testing.B) { + const players = 3 + const threshold = 2 + const bits = 4096 + const algo = crypto.SHA256 + benchmarkSignCombineHelper(rand.Reader, false, b, players, threshold, bits, algo, PKS1v15) +} + +func BenchmarkSignCombine_SHA256_4096_3_2_Scheme_BlindParallel(b *testing.B) { + const players = 3 + const threshold = 2 + const bits = 4096 + const algo = crypto.SHA256 + benchmarkSignCombineHelper(rand.Reader, true, b, players, threshold, bits, algo, PKS1v15) +} + +func BenchmarkSignCombine_SHA256_2048_3_2_Scheme(b *testing.B) { + const players = 3 + const threshold = 2 + const bits = 2048 + const algo = crypto.SHA256 + benchmarkSignCombineHelper(nil, false, b, players, threshold, bits, algo, PKS1v15) +} + +func BenchmarkSignCombine_SHA256_2048_3_2_Scheme_Blind(b *testing.B) { + const players = 3 + const threshold = 2 + const bits = 2048 + const algo = crypto.SHA256 + benchmarkSignCombineHelper(rand.Reader, false, b, players, threshold, bits, algo, PKS1v15) +} + +func BenchmarkSignCombine_SHA256_2048_3_2_Scheme_BlindParallel(b *testing.B) { + const players = 3 + const threshold = 2 + const bits = 2048 + const algo = crypto.SHA256 + benchmarkSignCombineHelper(rand.Reader, true, b, players, threshold, bits, algo, PKS1v15) +} + +func BenchmarkSignCombine_SHA256_1024_3_2_Scheme(b *testing.B) { + const players = 3 + const threshold = 2 + const bits = 1024 + const algo = crypto.SHA256 + benchmarkSignCombineHelper(nil, false, b, players, threshold, bits, algo, PKS1v15) +} + +func BenchmarkDealGeneration(b *testing.B) { + const players = 3 + const threshold = 2 + const bits = 2048 + key, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + b.Fatal("could not generate key") + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := Deal(rand.Reader, players, threshold, key, false) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/tss/rsa/signShare.go b/tss/rsa/signShare.go new file mode 100644 index 000000000..7207e28c3 --- /dev/null +++ b/tss/rsa/signShare.go @@ -0,0 +1,98 @@ +package rsa + +import ( + "encoding/binary" + "fmt" + "math" + "math/big" +) + +// 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. +type SignShare struct { + xi *big.Int + + Index uint + + Players uint + Threshold uint +} + +// 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) { + // | Players: uint16 | Threshold: uint16 | Index: uint16 | xiLen: uint16 | xi: []byte | + + if s.Players > math.MaxUint16 { + return nil, fmt.Errorf("rsa_threshold: signshare marshall: Players is too big to fit in a uint16") + } + + if s.Threshold > math.MaxUint16 { + return nil, fmt.Errorf("rsa_threshold: signshare marshall: Threshold is too big to fit in a uint16") + } + + if s.Index > math.MaxUint16 { + return nil, fmt.Errorf("rsa_threshold: signshare marshall: Index is too big to fit in a uint16") + } + + players := uint16(s.Players) + threshold := uint16(s.Threshold) + index := uint16(s.Index) + + xiBytes := s.xi.Bytes() + xiLen := len(xiBytes) + + if xiLen > math.MaxInt16 { + return nil, fmt.Errorf("rsa_threshold: signshare marshall: xiBytes is too big to fit it's length in a uint16") + } + + if xiLen == 0 { + xiLen = 1 + xiBytes = []byte{0} + } + + blen := 2 + 2 + 2 + 2 + xiLen + out := make([]byte, blen) + + binary.BigEndian.PutUint16(out[0:2], players) + binary.BigEndian.PutUint16(out[2:4], threshold) + binary.BigEndian.PutUint16(out[4:6], index) + + binary.BigEndian.PutUint16(out[6:8], uint16(xiLen)) + + copy(out[8:8+xiLen], xiBytes) + + return out, nil +} + +// UnmarshalBinary converts a byte array outputted from Marshall into a SignShare or returns an error if the value is invalid +func (s *SignShare) UnmarshalBinary(data []byte) error { + // | Players: uint16 | Threshold: uint16 | Index: uint16 | xiLen: uint16 | xi: []byte | + if len(data) < 8 { + return fmt.Errorf("rsa_threshold: signshare unmarshalKeyShareTest failed: data length was too short for reading Players, Threshold, Index, and xiLen") + } + + players := binary.BigEndian.Uint16(data[0:2]) + threshold := binary.BigEndian.Uint16(data[2:4]) + index := binary.BigEndian.Uint16(data[4:6]) + xiLen := binary.BigEndian.Uint16(data[6:8]) + + if xiLen == 0 { + return fmt.Errorf("rsa_threshold: signshare unmarshalKeyShareTest failed: xi is a required field but xiLen was 0") + } + + if uint16(len(data[8:])) < xiLen { + return fmt.Errorf("rsa_threshold: signshare unmarshalKeyShareTest failed: data length was too short for reading xi, needed: %d found: %d", xiLen, len(data[6:])) + } + + xi := big.Int{} + bytes := make([]byte, xiLen) + copy(bytes, data[8:8+xiLen]) + xi.SetBytes(bytes) + + s.Players = uint(players) + s.Threshold = uint(threshold) + s.Index = uint(index) + s.xi = &xi + + return nil +} diff --git a/tss/rsa/signShare_test.go b/tss/rsa/signShare_test.go new file mode 100644 index 000000000..e905d9638 --- /dev/null +++ b/tss/rsa/signShare_test.go @@ -0,0 +1,92 @@ +package rsa + +import ( + "crypto/rand" + "crypto/rsa" + "math/big" + "testing" +) + +func marshalTestSignShare(share SignShare, t *testing.T) { + marshall, err := share.MarshalBinary() + if err != nil { + t.Fatal(err) + } + + share2 := SignShare{} + err = share2.UnmarshalBinary(marshall) + if err != nil { + t.Fatal(err) + } + + if share.Players != share2.Players { + t.Fatalf("Players did not match, expected %d, found %d", share.Players, share2.Players) + } + + if share.Threshold != share2.Threshold { + t.Fatalf("Threshold did not match, expected %d, found %d", share.Threshold, share2.Threshold) + } + + if share.Index != share2.Index { + t.Fatalf("Index did not match, expected %d, found %d", share.Index, share2.Index) + } + + if share.xi.Cmp(share2.xi) != 0 { + t.Fatalf("si did not match, expected %v, found %v", share.xi.Bytes(), share2.xi.Bytes()) + } +} + +func unmarshalSignShareTest(t *testing.T, input []byte) { + share := SignShare{} + err := share.UnmarshalBinary(input) + if err == nil { + t.Fatalf("unmarshall succeeded when it shouldn't have") + } +} + +func TestMarshallSignShare(t *testing.T) { + marshalTestSignShare(SignShare{ + xi: big.NewInt(10), + Index: 30, + Players: 16, + Threshold: 18, + }, t) + + marshalTestSignShare(SignShare{ + xi: big.NewInt(0), + Index: 0, + Players: 0, + Threshold: 0, + }, t) + + unmarshalSignShareTest(t, []byte{}) + unmarshalSignShareTest(t, []byte{0, 0, 0}) + unmarshalSignShareTest(t, []byte{0, 0, 0, 0, 0, 0, 0, 0}) + unmarshalSignShareTest(t, []byte{0, 0, 0, 0, 0, 0, 0, 1}) + unmarshalSignShareTest(t, []byte{0, 0, 0, 0, 0, 0, 0, 2, 1}) +} + +func TestMarshallFullSignShare(t *testing.T) { + const players = 3 + const threshold = 2 + const bits = 4096 + + key, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + t.Fatal(err) + } + keys, err := Deal(rand.Reader, players, threshold, key, false) + if err != nil { + t.Fatal(err) + } + for _, share := range keys { + keyshare, err := share.Sign(rand.Reader, &key.PublicKey, []byte("Cloudflare!"), true) + if err != nil { + t.Fatal(err) + } + _, err = keyshare.MarshalBinary() + if err != nil { + t.Fatal(err) + } + } +} diff --git a/tss/rsa/util.go b/tss/rsa/util.go new file mode 100644 index 000000000..218032697 --- /dev/null +++ b/tss/rsa/util.go @@ -0,0 +1,12 @@ +package rsa + +import ( + "math/big" +) + +func calculateDelta(l int64) *big.Int { + // ∆ = l! + delta := big.Int{} + delta.MulRange(1, l) + return &delta +} diff --git a/tss/rsa/util_test.go b/tss/rsa/util_test.go new file mode 100644 index 000000000..3e8e9ce72 --- /dev/null +++ b/tss/rsa/util_test.go @@ -0,0 +1,21 @@ +package rsa + +import ( + "math/big" + "testing" +) + +func TestCalculateDelta(t *testing.T) { + ONE := big.NewInt(1) + if calculateDelta(0).Cmp(ONE) != 0 { + t.Fatal("calculateDelta failed on 0") + } + + if calculateDelta(1).Cmp(ONE) != 0 { + t.Fatal("calculateDelta failed on 1") + } + + if calculateDelta(5).Cmp(big.NewInt(120)) != 0 { + t.Fatal("calculateDelta failed on 5") + } +}