crypto: add sr25519 signature scheme (#4190)

* sr25519

* added amino encoding

* fixed dependencies

* Apply suggestions from code review

Co-Authored-By: Tess Rinearson <tess.rinearson@gmail.com>
Co-Authored-By: Marko <marbar3778@yahoo.com>

* file structure

* Apply suggestions from code review

Co-Authored-By: Anton Kaliaev <anton.kalyaev@gmail.com>

* address @melekes and @marbar3778 review

* removed nolint

* CHANGELOG and go-schnorrkel mod update
This commit is contained in:
Sunny Aggarwal
2019-12-05 09:41:38 +01:00
committed by Marko
co-authored by Tess Rinearson Marko Anton Kaliaev
parent 06b8752a46
commit b0bb8a1437
12 changed files with 322 additions and 4 deletions
+6 -4
View File
@@ -23,8 +23,10 @@ crypto `.Bytes()` uses Amino:binary encoding, but Amino:JSON is also supported.
```go
Example Amino:JSON encodings:
ed25519.PrivKeyEd25519 - {"type":"954568A3288910","value":"EVkqJO/jIXp3rkASXfh9YnyToYXRXhBr6g9cQVxPFnQBP/5povV4HTjvsy530kybxKHwEi85iU8YL0qQhSYVoQ=="}
ed25519.PubKeyEd25519 - {"type":"AC26791624DE60","value":"AT/+aaL1eB0477Mud9JMm8Sh8BIvOYlPGC9KkIUmFaE="}
crypto.PrivKeySecp256k1 - {"type":"019E82E1B0F798","value":"zx4Pnh67N+g2V+5vZbQzEyRerX9c4ccNZOVzM9RvJ0Y="}
crypto.PubKeySecp256k1 - {"type":"F8CCEAEB5AE980","value":"A8lPKJXcNl5VHt1FK8a244K9EJuS4WX1hFBnwisi0IJx"}
ed25519.PrivKeyEd25519 - {"type":"tendermint/PrivKeyEd25519","value":"EVkqJO/jIXp3rkASXfh9YnyToYXRXhBr6g9cQVxPFnQBP/5povV4HTjvsy530kybxKHwEi85iU8YL0qQhSYVoQ=="}
ed25519.PubKeyEd25519 - {"type":"tendermint/PubKeyEd25519","value":"AT/+aaL1eB0477Mud9JMm8Sh8BIvOYlPGC9KkIUmFaE="}
sr25519.PrivKeySr25519 - {"type":"tendermint/PrivKeySr25519","value":"xtYVH8UCIqfrY8FIFc0QEpAEBShSG4NT0zlEOVSZ2w4="}
sr25519.PubKeySr25519 - {"type":"tendermint/PubKeySr25519","value":"8sKBLKQ/OoXMcAJVxBqz1U7TyxRFQ5cmliuHy4MrF0s="}
crypto.PrivKeySecp256k1 - {"type":"tendermint/PrivKeySecp256k1","value":"zx4Pnh67N+g2V+5vZbQzEyRerX9c4ccNZOVzM9RvJ0Y="}
crypto.PubKeySecp256k1 - {"type":"tendermint/PubKeySecp256k1","value":"A8lPKJXcNl5VHt1FK8a244K9EJuS4WX1hFBnwisi0IJx"}
```
+6
View File
@@ -8,6 +8,7 @@ import (
"github.com/tendermint/tendermint/crypto/ed25519"
"github.com/tendermint/tendermint/crypto/multisig"
"github.com/tendermint/tendermint/crypto/secp256k1"
"github.com/tendermint/tendermint/crypto/sr25519"
)
var cdc = amino.NewCodec()
@@ -30,6 +31,7 @@ func init() {
// TODO: Have amino provide a way to go from concrete struct to route directly.
// Its currently a private API
nameTable[reflect.TypeOf(ed25519.PubKeyEd25519{})] = ed25519.PubKeyAminoName
nameTable[reflect.TypeOf(sr25519.PubKeySr25519{})] = sr25519.PubKeyAminoName
nameTable[reflect.TypeOf(secp256k1.PubKeySecp256k1{})] = secp256k1.PubKeyAminoName
nameTable[reflect.TypeOf(multisig.PubKeyMultisigThreshold{})] = multisig.PubKeyMultisigThresholdAminoRoute
}
@@ -48,6 +50,8 @@ func RegisterAmino(cdc *amino.Codec) {
cdc.RegisterInterface((*crypto.PubKey)(nil), nil)
cdc.RegisterConcrete(ed25519.PubKeyEd25519{},
ed25519.PubKeyAminoName, nil)
cdc.RegisterConcrete(sr25519.PubKeySr25519{},
sr25519.PubKeyAminoName, nil)
cdc.RegisterConcrete(secp256k1.PubKeySecp256k1{},
secp256k1.PubKeyAminoName, nil)
cdc.RegisterConcrete(multisig.PubKeyMultisigThreshold{},
@@ -56,6 +60,8 @@ func RegisterAmino(cdc *amino.Codec) {
cdc.RegisterInterface((*crypto.PrivKey)(nil), nil)
cdc.RegisterConcrete(ed25519.PrivKeyEd25519{},
ed25519.PrivKeyAminoName, nil)
cdc.RegisterConcrete(sr25519.PrivKeySr25519{},
sr25519.PrivKeyAminoName, nil)
cdc.RegisterConcrete(secp256k1.PrivKeySecp256k1{},
secp256k1.PrivKeyAminoName, nil)
}
+11
View File
@@ -13,6 +13,7 @@ import (
"github.com/tendermint/tendermint/crypto/ed25519"
"github.com/tendermint/tendermint/crypto/multisig"
"github.com/tendermint/tendermint/crypto/secp256k1"
"github.com/tendermint/tendermint/crypto/sr25519"
)
type byter interface {
@@ -57,9 +58,11 @@ func ExamplePrintRegisteredTypes() {
// Output: | Type | Name | Prefix | Length | Notes |
//| ---- | ---- | ------ | ----- | ------ |
//| PubKeyEd25519 | tendermint/PubKeyEd25519 | 0x1624DE64 | 0x20 | |
//| PubKeySr25519 | tendermint/PubKeySr25519 | 0x0DFB1005 | 0x20 | |
//| PubKeySecp256k1 | tendermint/PubKeySecp256k1 | 0xEB5AE987 | 0x21 | |
//| PubKeyMultisigThreshold | tendermint/PubKeyMultisigThreshold | 0x22C1F7E2 | variable | |
//| PrivKeyEd25519 | tendermint/PrivKeyEd25519 | 0xA3288910 | 0x40 | |
//| PrivKeySr25519 | tendermint/PrivKeySr25519 | 0x2F82D78B | 0x20 | |
//| PrivKeySecp256k1 | tendermint/PrivKeySecp256k1 | 0xE1B0F79B | 0x20 | |
}
@@ -74,6 +77,12 @@ func TestKeyEncodings(t *testing.T) {
pubSize: 37,
sigSize: 65,
},
{
privKey: sr25519.GenPrivKey(),
privSize: 37,
pubSize: 37,
sigSize: 65,
},
{
privKey: secp256k1.GenPrivKey(),
privSize: 37,
@@ -139,6 +148,7 @@ func TestPubkeyAminoName(t *testing.T) {
found bool
}{
{ed25519.PubKeyEd25519{}, ed25519.PubKeyAminoName, true},
{sr25519.PubKeySr25519{}, sr25519.PubKeyAminoName, true},
{secp256k1.PubKeySecp256k1{}, secp256k1.PubKeyAminoName, true},
{multisig.PubKeyMultisigThreshold{}, multisig.PubKeyMultisigThresholdAminoRoute, true},
}
@@ -219,6 +229,7 @@ func TestRegisterKeyType(t *testing.T) {
nameTable = make(map[reflect.Type]string, 3)
RegisterAmino(cdc)
nameTable[reflect.TypeOf(ed25519.PubKeyEd25519{})] = ed25519.PubKeyAminoName
nameTable[reflect.TypeOf(sr25519.PubKeySr25519{})] = sr25519.PubKeyAminoName
nameTable[reflect.TypeOf(secp256k1.PubKeySecp256k1{})] = secp256k1.PubKeyAminoName
nameTable[reflect.TypeOf(multisig.PubKeyMultisigThreshold{})] = multisig.PubKeyMultisigThresholdAminoRoute
}
+26
View File
@@ -0,0 +1,26 @@
package sr25519
import (
"io"
"testing"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/internal/benchmarking"
)
func BenchmarkKeyGeneration(b *testing.B) {
benchmarkKeygenWrapper := func(reader io.Reader) crypto.PrivKey {
return genPrivKey(reader)
}
benchmarking.BenchmarkKeyGeneration(b, benchmarkKeygenWrapper)
}
func BenchmarkSigning(b *testing.B) {
priv := GenPrivKey()
benchmarking.BenchmarkSigning(b, priv)
}
func BenchmarkVerification(b *testing.B) {
priv := GenPrivKey()
benchmarking.BenchmarkVerification(b, priv)
}
+29
View File
@@ -0,0 +1,29 @@
package sr25519
import (
amino "github.com/tendermint/go-amino"
"github.com/tendermint/tendermint/crypto"
)
var _ crypto.PrivKey = PrivKeySr25519{}
const (
PrivKeyAminoName = "tendermint/PrivKeySr25519"
PubKeyAminoName = "tendermint/PubKeySr25519"
// SignatureSize is the size of an Edwards25519 signature. Namely the size of a compressed
// Sr25519 point, and a field element. Both of which are 32 bytes.
SignatureSize = 64
)
var cdc = amino.NewCodec()
func init() {
cdc.RegisterInterface((*crypto.PubKey)(nil), nil)
cdc.RegisterConcrete(PubKeySr25519{},
PubKeyAminoName, nil)
cdc.RegisterInterface((*crypto.PrivKey)(nil), nil)
cdc.RegisterConcrete(PrivKeySr25519{},
PrivKeyAminoName, nil)
}
+99
View File
@@ -0,0 +1,99 @@
package sr25519
import (
"crypto/subtle"
"fmt"
"io"
"github.com/tendermint/tendermint/crypto"
schnorrkel "github.com/ChainSafe/go-schnorrkel"
)
// PrivKeySr25519Size is the number of bytes in an Sr25519 private key.
const PrivKeySr25519Size = 32
// PrivKeySr25519 implements crypto.PrivKey.
type PrivKeySr25519 [PrivKeySr25519Size]byte
// Bytes marshals the privkey using amino encoding.
func (privKey PrivKeySr25519) Bytes() []byte {
return cdc.MustMarshalBinaryBare(privKey)
}
// Sign produces a signature on the provided message.
func (privKey PrivKeySr25519) Sign(msg []byte) ([]byte, error) {
secretKey := &(schnorrkel.SecretKey{})
err := secretKey.Decode(privKey)
if err != nil {
return []byte{}, err
}
signingContext := schnorrkel.NewSigningContext([]byte{}, msg)
sig, err := secretKey.Sign(signingContext)
if err != nil {
return []byte{}, err
}
sigBytes := sig.Encode()
return sigBytes[:], nil
}
// PubKey gets the corresponding public key from the private key.
func (privKey PrivKeySr25519) PubKey() crypto.PubKey {
secretKey := &(schnorrkel.SecretKey{})
err := secretKey.Decode(privKey)
if err != nil {
panic(fmt.Sprintf("Invalid private key: %v", err))
}
pubkey, _ := secretKey.Public()
return PubKeySr25519(pubkey.Encode())
}
// Equals - you probably don't need to use this.
// Runs in constant time based on length of the keys.
func (privKey PrivKeySr25519) Equals(other crypto.PrivKey) bool {
if otherEd, ok := other.(PrivKeySr25519); ok {
return subtle.ConstantTimeCompare(privKey[:], otherEd[:]) == 1
} else {
return false
}
}
// GenPrivKey generates a new sr25519 private key.
// It uses OS randomness in conjunction with the current global random seed
// in tendermint/libs/common to generate the private key.
func GenPrivKey() PrivKeySr25519 {
return genPrivKey(crypto.CReader())
}
// genPrivKey generates a new sr25519 private key using the provided reader.
func genPrivKey(rand io.Reader) PrivKeySr25519 {
var seed [64]byte
out := make([]byte, 64)
_, err := io.ReadFull(rand, out)
if err != nil {
panic(err)
}
copy(seed[:], out)
return schnorrkel.NewMiniSecretKey(seed).ExpandEd25519().Encode()
}
// GenPrivKeyFromSecret hashes the secret with SHA2, and uses
// that 32 byte output to create the private key.
// NOTE: secret should be the output of a KDF like bcrypt,
// if it's derived from user input.
func GenPrivKeyFromSecret(secret []byte) PrivKeySr25519 {
seed := crypto.Sha256(secret) // Not Ripemd160 because we want 32 bytes.
var bz [PrivKeySr25519Size]byte
copy(bz[:], seed)
privKey, _ := schnorrkel.NewMiniSecretKeyFromRaw(bz)
return privKey.ExpandEd25519().Encode()
}
+72
View File
@@ -0,0 +1,72 @@
package sr25519
import (
"bytes"
"fmt"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/tmhash"
schnorrkel "github.com/ChainSafe/go-schnorrkel"
)
var _ crypto.PubKey = PubKeySr25519{}
// PubKeySr25519Size is the number of bytes in an Sr25519 public key.
const PubKeySr25519Size = 32
// PubKeySr25519 implements crypto.PubKey for the Sr25519 signature scheme.
type PubKeySr25519 [PubKeySr25519Size]byte
// Address is the SHA256-20 of the raw pubkey bytes.
func (pubKey PubKeySr25519) Address() crypto.Address {
return crypto.Address(tmhash.SumTruncated(pubKey[:]))
}
// Bytes marshals the PubKey using amino encoding.
func (pubKey PubKeySr25519) Bytes() []byte {
bz, err := cdc.MarshalBinaryBare(pubKey)
if err != nil {
panic(err)
}
return bz
}
func (pubKey PubKeySr25519) VerifyBytes(msg []byte, sig []byte) bool {
// make sure we use the same algorithm to sign
if len(sig) != SignatureSize {
return false
}
var sig64 [SignatureSize]byte
copy(sig64[:], sig)
publicKey := &(schnorrkel.PublicKey{})
err := publicKey.Decode(pubKey)
if err != nil {
return false
}
signingContext := schnorrkel.NewSigningContext([]byte{}, msg)
signature := &(schnorrkel.Signature{})
err = signature.Decode(sig64)
if err != nil {
return false
}
return publicKey.Verify(signature, signingContext)
}
func (pubKey PubKeySr25519) String() string {
return fmt.Sprintf("PubKeySr25519{%X}", pubKey[:])
}
// Equals - checks that two public keys are the same time
// Runs in constant time based on length of the keys.
func (pubKey PubKeySr25519) Equals(other crypto.PubKey) bool {
if otherEd, ok := other.(PubKeySr25519); ok {
return bytes.Equal(pubKey[:], otherEd[:])
} else {
return false
}
}
+31
View File
@@ -0,0 +1,31 @@
package sr25519_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/sr25519"
)
func TestSignAndValidateSr25519(t *testing.T) {
privKey := sr25519.GenPrivKey()
pubKey := privKey.PubKey()
msg := crypto.CRandBytes(128)
sig, err := privKey.Sign(msg)
require.Nil(t, err)
// Test the signature
assert.True(t, pubKey.VerifyBytes(msg, sig))
assert.True(t, pubKey.VerifyBytes(msg, sig))
// Mutate the signature, just one bit.
// TODO: Replace this with a much better fuzzer, tendermint/ed25519/issues/10
sig[7] ^= byte(0x01)
assert.False(t, pubKey.VerifyBytes(msg, sig))
}