@ -1,25 +0,0 @@ | |||
/* | |||
package cryptostore maintains everything needed for doing public-key signing and | |||
key management in software, based on the go-crypto library from tendermint. | |||
It is flexible, and allows the user to provide a key generation algorithm | |||
(currently Ed25519 or Secp256k1), an encoder to passphrase-encrypt our keys | |||
when storing them (currently SecretBox from NaCl), and a method to persist | |||
the keys (currently FileStorage like ssh, or MemStorage for tests). | |||
It should be relatively simple to write your own implementation of these | |||
interfaces to match your specific security requirements. | |||
Note that the private keys are never exposed outside the package, and the | |||
interface of Manager could be implemented by an HSM in the future for | |||
enhanced security. It would require a completely different implementation | |||
however. | |||
This Manager aims to implement Signer and KeyManager interfaces, along | |||
with some extensions to allow importing/exporting keys and updating the | |||
passphrase. | |||
Encoder and Generator implementations are currently in this package, | |||
keys.Storage implementations exist as subpackages of | |||
keys/storage | |||
*/ | |||
package cryptostore |
@ -1,49 +0,0 @@ | |||
package cryptostore | |||
import ( | |||
crypto "github.com/tendermint/go-crypto" | |||
keys "github.com/tendermint/go-crypto/keys" | |||
) | |||
// encryptedStorage needs passphrase to get private keys | |||
type encryptedStorage struct { | |||
coder Encoder | |||
store keys.Storage | |||
} | |||
func (es encryptedStorage) Put(name, pass string, key crypto.PrivKey) error { | |||
secret, err := es.coder.Encrypt(key, pass) | |||
if err != nil { | |||
return err | |||
} | |||
ki := info(name, key) | |||
return es.store.Put(name, secret, ki) | |||
} | |||
func (es encryptedStorage) Get(name, pass string) (crypto.PrivKey, keys.Info, error) { | |||
secret, info, err := es.store.Get(name) | |||
if err != nil { | |||
return crypto.PrivKey{}, info, err | |||
} | |||
key, err := es.coder.Decrypt(secret, pass) | |||
return key, info, err | |||
} | |||
func (es encryptedStorage) List() (keys.Infos, error) { | |||
return es.store.List() | |||
} | |||
func (es encryptedStorage) Delete(name string) error { | |||
return es.store.Delete(name) | |||
} | |||
// info hardcodes the encoding of keys | |||
func info(name string, key crypto.PrivKey) keys.Info { | |||
pub := key.PubKey() | |||
return keys.Info{ | |||
Name: name, | |||
Address: pub.Address(), | |||
PubKey: pub, | |||
} | |||
} |
@ -1,60 +0,0 @@ | |||
package cryptostore | |||
import ( | |||
"github.com/pkg/errors" | |||
crypto "github.com/tendermint/go-crypto" | |||
) | |||
var ( | |||
// SecretBox uses the algorithm from NaCL to store secrets securely | |||
SecretBox Encoder = secretbox{} | |||
// Noop doesn't do any encryption, should only be used in test code | |||
Noop Encoder = noop{} | |||
) | |||
// Encoder is used to encrypt any key with a passphrase for storage. | |||
// | |||
// This should use a well-designed symetric encryption algorithm | |||
type Encoder interface { | |||
Encrypt(key crypto.PrivKey, pass string) ([]byte, error) | |||
Decrypt(data []byte, pass string) (crypto.PrivKey, error) | |||
} | |||
func secret(passphrase string) []byte { | |||
// TODO: Sha256(Bcrypt(passphrase)) | |||
return crypto.Sha256([]byte(passphrase)) | |||
} | |||
type secretbox struct{} | |||
func (e secretbox) Encrypt(key crypto.PrivKey, pass string) ([]byte, error) { | |||
if pass == "" { | |||
return key.Bytes(), nil | |||
} | |||
s := secret(pass) | |||
cipher := crypto.EncryptSymmetric(key.Bytes(), s) | |||
return cipher, nil | |||
} | |||
func (e secretbox) Decrypt(data []byte, pass string) (key crypto.PrivKey, err error) { | |||
private := data | |||
if pass != "" { | |||
s := secret(pass) | |||
private, err = crypto.DecryptSymmetric(data, s) | |||
if err != nil { | |||
return crypto.PrivKey{}, errors.Wrap(err, "Invalid Passphrase") | |||
} | |||
} | |||
key, err = crypto.PrivKeyFromBytes(private) | |||
return key, errors.Wrap(err, "Invalid Passphrase") | |||
} | |||
type noop struct{} | |||
func (n noop) Encrypt(key crypto.PrivKey, pass string) ([]byte, error) { | |||
return key.Bytes(), nil | |||
} | |||
func (n noop) Decrypt(data []byte, pass string) (crypto.PrivKey, error) { | |||
return crypto.PrivKeyFromBytes(data) | |||
} |
@ -1,105 +0,0 @@ | |||
package cryptostore_test | |||
import ( | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/require" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
"github.com/tendermint/go-crypto/keys/cryptostore" | |||
) | |||
func TestNoopEncoder(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
noop := cryptostore.Noop | |||
key, err := cryptostore.GenEd25519.Generate(cmn.RandBytes(16)) | |||
require.NoError(err) | |||
key2, err := cryptostore.GenSecp256k1.Generate(cmn.RandBytes(16)) | |||
require.NoError(err) | |||
b, err := noop.Encrypt(key, "encode") | |||
require.Nil(err) | |||
assert.NotEmpty(b) | |||
b2, err := noop.Encrypt(key2, "encode") | |||
require.Nil(err) | |||
assert.NotEmpty(b2) | |||
assert.NotEqual(b, b2) | |||
// note the decode with a different password works - not secure! | |||
pk, err := noop.Decrypt(b, "decode") | |||
require.Nil(err) | |||
require.NotNil(pk) | |||
assert.Equal(key, pk) | |||
pk2, err := noop.Decrypt(b2, "kggugougp") | |||
require.Nil(err) | |||
require.NotNil(pk2) | |||
assert.Equal(key2, pk2) | |||
} | |||
func TestSecretBox(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
enc := cryptostore.SecretBox | |||
key, err := cryptostore.GenEd25519.Generate(cmn.RandBytes(16)) | |||
require.NoError(err) | |||
pass := "some-special-secret" | |||
b, err := enc.Encrypt(key, pass) | |||
require.Nil(err) | |||
assert.NotEmpty(b) | |||
// decoding with a different pass is an error | |||
pk, err := enc.Decrypt(b, "decode") | |||
require.NotNil(err) | |||
require.True(pk.Empty()) | |||
// but decoding with the same passphrase gets us our key | |||
pk, err = enc.Decrypt(b, pass) | |||
require.Nil(err) | |||
assert.Equal(key, pk) | |||
} | |||
func TestSecretBoxNoPass(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
enc := cryptostore.SecretBox | |||
key, rerr := cryptostore.GenEd25519.Generate(cmn.RandBytes(16)) | |||
require.NoError(rerr) | |||
cases := []struct { | |||
encode string | |||
decode string | |||
valid bool | |||
}{ | |||
{"foo", "foo", true}, | |||
{"foo", "food", false}, | |||
{"", "", true}, | |||
{"", "a", false}, | |||
{"a", "", false}, | |||
} | |||
for i, tc := range cases { | |||
b, err := enc.Encrypt(key, tc.encode) | |||
require.Nil(err, "%d: %+v", i, err) | |||
assert.NotEmpty(b, "%d", i) | |||
pk, err := enc.Decrypt(b, tc.decode) | |||
if tc.valid { | |||
require.Nil(err, "%d: %+v", i, err) | |||
assert.Equal(key, pk, "%d", i) | |||
} else { | |||
require.NotNil(err, "%d", i) | |||
} | |||
} | |||
// now let's make sure raw bytes also work... | |||
b := key.Bytes() | |||
pk, err := enc.Decrypt(b, "") | |||
require.Nil(err, "%+v", err) | |||
assert.Equal(key, pk) | |||
} |
@ -1,88 +0,0 @@ | |||
package cryptostore | |||
import ( | |||
"github.com/pkg/errors" | |||
crypto "github.com/tendermint/go-crypto" | |||
"github.com/tendermint/go-crypto/nano" | |||
) | |||
var ( | |||
// GenEd25519 produces Ed25519 private keys | |||
GenEd25519 Generator = GenFunc(genEd25519) | |||
// GenSecp256k1 produces Secp256k1 private keys | |||
GenSecp256k1 Generator = GenFunc(genSecp256) | |||
// GenLedgerEd25519 used Ed25519 keys stored on nano ledger s with cosmos app | |||
GenLedgerEd25519 Generator = GenFunc(genLedgerEd25519) | |||
) | |||
// Generator determines the type of private key the keystore creates | |||
type Generator interface { | |||
Generate(secret []byte) (crypto.PrivKey, error) | |||
} | |||
// GenFunc is a helper to transform a function into a Generator | |||
type GenFunc func(secret []byte) (crypto.PrivKey, error) | |||
func (f GenFunc) Generate(secret []byte) (crypto.PrivKey, error) { | |||
return f(secret) | |||
} | |||
func genEd25519(secret []byte) (crypto.PrivKey, error) { | |||
key := crypto.GenPrivKeyEd25519FromSecret(secret).Wrap() | |||
return key, nil | |||
} | |||
func genSecp256(secret []byte) (crypto.PrivKey, error) { | |||
key := crypto.GenPrivKeySecp256k1FromSecret(secret).Wrap() | |||
return key, nil | |||
} | |||
// secret is completely ignored for the ledger... | |||
// just for interface compatibility | |||
func genLedgerEd25519(secret []byte) (crypto.PrivKey, error) { | |||
return nano.NewPrivKeyLedgerEd25519Ed25519() | |||
} | |||
type genInvalidByte struct { | |||
typ byte | |||
} | |||
func (g genInvalidByte) Generate(secret []byte) (crypto.PrivKey, error) { | |||
err := errors.Errorf("Cannot generate keys for algorithm: %X", g.typ) | |||
return crypto.PrivKey{}, err | |||
} | |||
type genInvalidAlgo struct { | |||
algo string | |||
} | |||
func (g genInvalidAlgo) Generate(secret []byte) (crypto.PrivKey, error) { | |||
err := errors.Errorf("Cannot generate keys for algorithm: %s", g.algo) | |||
return crypto.PrivKey{}, err | |||
} | |||
func getGenerator(algo string) Generator { | |||
switch algo { | |||
case crypto.NameEd25519: | |||
return GenEd25519 | |||
case crypto.NameSecp256k1: | |||
return GenSecp256k1 | |||
case nano.NameLedgerEd25519: | |||
return GenLedgerEd25519 | |||
default: | |||
return genInvalidAlgo{algo} | |||
} | |||
} | |||
func getGeneratorByType(typ byte) Generator { | |||
switch typ { | |||
case crypto.TypeEd25519: | |||
return GenEd25519 | |||
case crypto.TypeSecp256k1: | |||
return GenSecp256k1 | |||
case nano.TypeLedgerEd25519: | |||
return GenLedgerEd25519 | |||
default: | |||
return genInvalidByte{typ} | |||
} | |||
} |
@ -1,169 +0,0 @@ | |||
package cryptostore | |||
import ( | |||
"strings" | |||
crypto "github.com/tendermint/go-crypto" | |||
keys "github.com/tendermint/go-crypto/keys" | |||
) | |||
// Manager combines encyption and storage implementation to provide | |||
// a full-featured key manager | |||
type Manager struct { | |||
es encryptedStorage | |||
codec keys.Codec | |||
} | |||
func New(coder Encoder, store keys.Storage, codec keys.Codec) Manager { | |||
return Manager{ | |||
es: encryptedStorage{ | |||
coder: coder, | |||
store: store, | |||
}, | |||
codec: codec, | |||
} | |||
} | |||
// assert Manager satisfies keys.Signer and keys.Manager interfaces | |||
var _ keys.Signer = Manager{} | |||
var _ keys.Manager = Manager{} | |||
// Create adds a new key to the storage engine, returning error if | |||
// another key already stored under this name | |||
// | |||
// algo must be a supported go-crypto algorithm: ed25519, secp256k1 | |||
func (s Manager) Create(name, passphrase, algo string) (keys.Info, string, error) { | |||
// 128-bits are the all the randomness we can make use of | |||
secret := crypto.CRandBytes(16) | |||
gen := getGenerator(algo) | |||
key, err := gen.Generate(secret) | |||
if err != nil { | |||
return keys.Info{}, "", err | |||
} | |||
err = s.es.Put(name, passphrase, key) | |||
if err != nil { | |||
return keys.Info{}, "", err | |||
} | |||
// we append the type byte to the serialized secret to help with recovery | |||
// ie [secret] = [secret] + [type] | |||
typ := key.Bytes()[0] | |||
secret = append(secret, typ) | |||
seed, err := s.codec.BytesToWords(secret) | |||
phrase := strings.Join(seed, " ") | |||
return info(name, key), phrase, err | |||
} | |||
// Recover takes a seed phrase and tries to recover the private key. | |||
// | |||
// If the seed phrase is valid, it will create the private key and store | |||
// it under name, protected by passphrase. | |||
// | |||
// Result similar to New(), except it doesn't return the seed again... | |||
func (s Manager) Recover(name, passphrase, seedphrase string) (keys.Info, error) { | |||
words := strings.Split(strings.TrimSpace(seedphrase), " ") | |||
secret, err := s.codec.WordsToBytes(words) | |||
if err != nil { | |||
return keys.Info{}, err | |||
} | |||
// secret is comprised of the actual secret with the type appended | |||
// ie [secret] = [secret] + [type] | |||
l := len(secret) | |||
secret, typ := secret[:l-1], secret[l-1] | |||
gen := getGeneratorByType(typ) | |||
key, err := gen.Generate(secret) | |||
if err != nil { | |||
return keys.Info{}, err | |||
} | |||
// d00d, it worked! create the bugger.... | |||
err = s.es.Put(name, passphrase, key) | |||
return info(name, key), err | |||
} | |||
// List loads the keys from the storage and enforces alphabetical order | |||
func (s Manager) List() (keys.Infos, error) { | |||
res, err := s.es.List() | |||
res.Sort() | |||
return res, err | |||
} | |||
// Get returns the public information about one key | |||
func (s Manager) Get(name string) (keys.Info, error) { | |||
_, info, err := s.es.store.Get(name) | |||
return info, err | |||
} | |||
// Sign will modify the Signable in order to attach a valid signature with | |||
// this public key | |||
// | |||
// If no key for this name, or the passphrase doesn't match, returns an error | |||
func (s Manager) Sign(name, passphrase string, tx keys.Signable) error { | |||
key, _, err := s.es.Get(name, passphrase) | |||
if err != nil { | |||
return err | |||
} | |||
sig := key.Sign(tx.SignBytes()) | |||
pubkey := key.PubKey() | |||
return tx.Sign(pubkey, sig) | |||
} | |||
// Export decodes the private key with the current password, encodes | |||
// it with a secure one-time password and generates a sequence that can be | |||
// Imported by another Manager | |||
// | |||
// This is designed to copy from one device to another, or provide backups | |||
// during version updates. | |||
func (s Manager) Export(name, oldpass, transferpass string) ([]byte, error) { | |||
key, _, err := s.es.Get(name, oldpass) | |||
if err != nil { | |||
return nil, err | |||
} | |||
res, err := s.es.coder.Encrypt(key, transferpass) | |||
return res, err | |||
} | |||
// Import accepts bytes generated by Export along with the same transferpass | |||
// If they are valid, it stores the password under the given name with the | |||
// new passphrase. | |||
func (s Manager) Import(name, newpass, transferpass string, data []byte) error { | |||
key, err := s.es.coder.Decrypt(data, transferpass) | |||
if err != nil { | |||
return err | |||
} | |||
return s.es.Put(name, newpass, key) | |||
} | |||
// Delete removes key forever, but we must present the | |||
// proper passphrase before deleting it (for security) | |||
func (s Manager) Delete(name, passphrase string) error { | |||
// verify we have the proper password before deleting | |||
_, _, err := s.es.Get(name, passphrase) | |||
if err != nil { | |||
return err | |||
} | |||
return s.es.Delete(name) | |||
} | |||
// Update changes the passphrase with which a already stored key is encoded. | |||
// | |||
// oldpass must be the current passphrase used for encoding, newpass will be | |||
// the only valid passphrase from this time forward | |||
func (s Manager) Update(name, oldpass, newpass string) error { | |||
key, _, err := s.es.Get(name, oldpass) | |||
if err != nil { | |||
return err | |||
} | |||
// we must delete first, as Putting over an existing name returns an error | |||
s.Delete(name, oldpass) | |||
return s.es.Put(name, newpass, key) | |||
} |
@ -1,48 +0,0 @@ | |||
package cryptostore | |||
import ( | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
crypto "github.com/tendermint/go-crypto" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
keys "github.com/tendermint/go-crypto/keys" | |||
) | |||
func TestSortKeys(t *testing.T) { | |||
assert := assert.New(t) | |||
gen := func() crypto.PrivKey { | |||
key, _ := GenEd25519.Generate(cmn.RandBytes(16)) | |||
return key | |||
} | |||
assert.NotEqual(gen(), gen()) | |||
// alphabetical order is n3, n1, n2 | |||
n1, n2, n3 := "john", "mike", "alice" | |||
infos := keys.Infos{ | |||
info(n1, gen()), | |||
info(n2, gen()), | |||
info(n3, gen()), | |||
} | |||
// make sure they are initialized unsorted | |||
assert.Equal(n1, infos[0].Name) | |||
assert.Equal(n2, infos[1].Name) | |||
assert.Equal(n3, infos[2].Name) | |||
// now they are sorted | |||
infos.Sort() | |||
assert.Equal(n3, infos[0].Name) | |||
assert.Equal(n1, infos[1].Name) | |||
assert.Equal(n2, infos[2].Name) | |||
// make sure info put some real data there... | |||
assert.NotEmpty(infos[0].PubKey) | |||
assert.NotEmpty(infos[0].PubKey.Address()) | |||
assert.NotEmpty(infos[1].PubKey) | |||
assert.NotEmpty(infos[1].PubKey.Address()) | |||
assert.NotEqual(infos[0].PubKey, infos[1].PubKey) | |||
} |
@ -0,0 +1,246 @@ | |||
package keys | |||
import ( | |||
"fmt" | |||
"strings" | |||
"github.com/pkg/errors" | |||
crypto "github.com/tendermint/go-crypto" | |||
dbm "github.com/tendermint/tmlibs/db" | |||
"github.com/tendermint/go-crypto/nano" | |||
) | |||
// XXX Lets use go-crypto/bcrypt and ascii encoding directly in here without | |||
// further wrappers around a store or DB. | |||
// Copy functions from: https://github.com/tendermint/mintkey/blob/master/cmd/mintkey/common.go | |||
// | |||
// dbKeybase combines encyption and storage implementation to provide | |||
// a full-featured key manager | |||
type dbKeybase struct { | |||
db dbm.DB | |||
codec Codec | |||
} | |||
func New(db dbm.DB, codec Codec) dbKeybase { | |||
return dbKeybase{ | |||
db: db, | |||
codec: codec, | |||
} | |||
} | |||
var _ Keybase = dbKeybase{} | |||
// Create adds a new key to the storage engine, returning error if | |||
// another key already stored under this name | |||
// | |||
// algo must be a supported go-crypto algorithm: ed25519, secp256k1 | |||
func (kb dbKeybase) Create(name, passphrase, algo string) (Info, string, error) { | |||
// 128-bits are the all the randomness we can make use of | |||
secret := crypto.CRandBytes(16) | |||
key, err := generate(algo, secret) | |||
if err != nil { | |||
return Info{}, "", err | |||
} | |||
public := kb.writeKey(key, name, passphrase) | |||
// we append the type byte to the serialized secret to help with recovery | |||
// ie [secret] = [secret] + [type] | |||
typ := key.Bytes()[0] | |||
secret = append(secret, typ) | |||
seed, err := kb.codec.BytesToWords(secret) | |||
phrase := strings.Join(seed, " ") | |||
return public, phrase, err | |||
} | |||
// Recover takes a seed phrase and tries to recover the private key. | |||
// | |||
// If the seed phrase is valid, it will create the private key and store | |||
// it under name, protected by passphrase. | |||
// | |||
// Result similar to New(), except it doesn't return the seed again... | |||
func (kb dbKeybase) Recover(name, passphrase, seedphrase string) (Info, error) { | |||
words := strings.Split(strings.TrimSpace(seedphrase), " ") | |||
secret, err := kb.codec.WordsToBytes(words) | |||
if err != nil { | |||
return Info{}, err | |||
} | |||
// secret is comprised of the actual secret with the type appended | |||
// ie [secret] = [secret] + [type] | |||
l := len(secret) | |||
secret, typ := secret[:l-1], secret[l-1] | |||
key, err := generateByType(typ, secret) | |||
if err != nil { | |||
return Info{}, err | |||
} | |||
// d00d, it worked! create the bugger.... | |||
public := kb.writeKey(key, name, passphrase) | |||
return public, err | |||
} | |||
// List loads the keys from the storage and enforces alphabetical order | |||
func (kb dbKeybase) List() ([]Info, error) { | |||
var res []Info | |||
for iter := kb.db.Iterator(); iter.Valid(); iter.Next() { | |||
key := iter.Key() | |||
if isPub(key) { | |||
info, err := readInfo(iter.Value()) | |||
if err != nil { | |||
return nil, err | |||
} | |||
res = append(res, info) | |||
} | |||
} | |||
return res, nil | |||
} | |||
// Get returns the public information about one key | |||
func (kb dbKeybase) Get(name string) (Info, error) { | |||
bs := kb.db.Get(pubName(name)) | |||
return readInfo(bs) | |||
} | |||
// Sign will modify the Signable in order to attach a valid signature with | |||
// this public key | |||
// | |||
// If no key for this name, or the passphrase doesn't match, returns an error | |||
func (kb dbKeybase) Sign(name, passphrase string, msg []byte) (sig crypto.Signature, pk crypto.PubKey, err error) { | |||
var key crypto.PrivKey | |||
bs := kb.db.Get(privName(name)) | |||
key, err = unarmorDecryptPrivKey(string(bs), passphrase) | |||
if err != nil { | |||
return | |||
} | |||
sig = key.Sign(msg) | |||
pk = key.PubKey() | |||
return | |||
} | |||
// Export decodes the private key with the current password, encodes | |||
// it with a secure one-time password and generates a sequence that can be | |||
// Imported by another dbKeybase | |||
// | |||
// This is designed to copy from one device to another, or provide backups | |||
// during version updates. | |||
func (kb dbKeybase) Export(name, oldpass, transferpass string) ([]byte, error) { | |||
bs := kb.db.Get(privName(name)) | |||
key, err := unarmorDecryptPrivKey(string(bs), oldpass) | |||
if err != nil { | |||
return nil, err | |||
} | |||
if transferpass == "" { | |||
return key.Bytes(), nil | |||
} | |||
res := encryptArmorPrivKey(key, transferpass) | |||
return []byte(res), nil | |||
} | |||
// Import accepts bytes generated by Export along with the same transferpass | |||
// If they are valid, it stores the password under the given name with the | |||
// new passphrase. | |||
func (kb dbKeybase) Import(name, newpass, transferpass string, data []byte) (err error) { | |||
var key crypto.PrivKey | |||
if transferpass == "" { | |||
key, err = crypto.PrivKeyFromBytes(data) | |||
} else { | |||
key, err = unarmorDecryptPrivKey(string(data), transferpass) | |||
} | |||
if err != nil { | |||
return err | |||
} | |||
kb.writeKey(key, name, newpass) | |||
return nil | |||
} | |||
// Delete removes key forever, but we must present the | |||
// proper passphrase before deleting it (for security) | |||
func (kb dbKeybase) Delete(name, passphrase string) error { | |||
// verify we have the proper password before deleting | |||
bs := kb.db.Get(privName(name)) | |||
_, err := unarmorDecryptPrivKey(string(bs), passphrase) | |||
if err != nil { | |||
return err | |||
} | |||
kb.db.DeleteSync(pubName(name)) | |||
kb.db.DeleteSync(privName(name)) | |||
return nil | |||
} | |||
// Update changes the passphrase with which a already stored key is encoded. | |||
// | |||
// oldpass must be the current passphrase used for encoding, newpass will be | |||
// the only valid passphrase from this time forward | |||
func (kb dbKeybase) Update(name, oldpass, newpass string) error { | |||
bs := kb.db.Get(privName(name)) | |||
key, err := unarmorDecryptPrivKey(string(bs), oldpass) | |||
if err != nil { | |||
return err | |||
} | |||
// we must delete first, as Putting over an existing name returns an error | |||
kb.db.DeleteSync(pubName(name)) | |||
kb.db.DeleteSync(privName(name)) | |||
kb.writeKey(key, name, newpass) | |||
return nil | |||
} | |||
func (kb dbKeybase) writeKey(priv crypto.PrivKey, name, passphrase string) Info { | |||
// generate the public bytes | |||
public := info(name, priv) | |||
// generate the encrypted privkey | |||
private := encryptArmorPrivKey(priv, passphrase) | |||
// write them both | |||
kb.db.SetSync(pubName(name), public.bytes()) | |||
kb.db.SetSync(privName(name), []byte(private)) | |||
return public | |||
} | |||
func generate(algo string, secret []byte) (crypto.PrivKey, error) { | |||
switch algo { | |||
case crypto.NameEd25519: | |||
return crypto.GenPrivKeyEd25519FromSecret(secret).Wrap(), nil | |||
case crypto.NameSecp256k1: | |||
return crypto.GenPrivKeySecp256k1FromSecret(secret).Wrap(), nil | |||
case nano.NameLedgerEd25519: | |||
return nano.NewPrivKeyLedgerEd25519Ed25519() | |||
default: | |||
err := errors.Errorf("Cannot generate keys for algorithm: %s", algo) | |||
return crypto.PrivKey{}, err | |||
} | |||
} | |||
func generateByType(typ byte, secret []byte) (crypto.PrivKey, error) { | |||
switch typ { | |||
case crypto.TypeEd25519: | |||
return crypto.GenPrivKeyEd25519FromSecret(secret).Wrap(), nil | |||
case crypto.TypeSecp256k1: | |||
return crypto.GenPrivKeySecp256k1FromSecret(secret).Wrap(), nil | |||
case nano.TypeLedgerEd25519: | |||
return nano.NewPrivKeyLedgerEd25519Ed25519() | |||
default: | |||
err := errors.Errorf("Cannot generate keys for algorithm: %X", typ) | |||
return crypto.PrivKey{}, err | |||
} | |||
} | |||
func pubName(name string) []byte { | |||
return []byte(fmt.Sprintf("%s.pub", name)) | |||
} | |||
func privName(name string) []byte { | |||
return []byte(fmt.Sprintf("%s.priv", name)) | |||
} | |||
func isPub(name []byte) bool { | |||
return strings.HasSuffix(string(name), ".pub") | |||
} |
@ -0,0 +1,73 @@ | |||
package keys | |||
import ( | |||
"encoding/hex" | |||
"fmt" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
"github.com/tendermint/go-crypto" | |||
"github.com/tendermint/go-crypto/bcrypt" | |||
) | |||
const ( | |||
blockTypePrivKey = "TENDERMINT PRIVATE KEY" | |||
) | |||
func encryptArmorPrivKey(privKey crypto.PrivKey, passphrase string) string { | |||
saltBytes, encBytes := encryptPrivKey(privKey, passphrase) | |||
header := map[string]string{ | |||
"kdf": "bcrypt", | |||
"salt": fmt.Sprintf("%X", saltBytes), | |||
} | |||
armorStr := crypto.EncodeArmor(blockTypePrivKey, header, encBytes) | |||
return armorStr | |||
} | |||
func unarmorDecryptPrivKey(armorStr string, passphrase string) (crypto.PrivKey, error) { | |||
var privKey crypto.PrivKey | |||
blockType, header, encBytes, err := crypto.DecodeArmor(armorStr) | |||
if err != nil { | |||
return privKey, err | |||
} | |||
if blockType != blockTypePrivKey { | |||
return privKey, fmt.Errorf("Unrecognized armor type: %v", blockType) | |||
} | |||
if header["kdf"] != "bcrypt" { | |||
return privKey, fmt.Errorf("Unrecognized KDF type: %v", header["KDF"]) | |||
} | |||
if header["salt"] == "" { | |||
return privKey, fmt.Errorf("Missing salt bytes") | |||
} | |||
saltBytes, err := hex.DecodeString(header["salt"]) | |||
if err != nil { | |||
return privKey, fmt.Errorf("Error decoding salt: %v", err.Error()) | |||
} | |||
privKey, err = decryptPrivKey(saltBytes, encBytes, passphrase) | |||
return privKey, err | |||
} | |||
func encryptPrivKey(privKey crypto.PrivKey, passphrase string) (saltBytes []byte, encBytes []byte) { | |||
saltBytes = crypto.CRandBytes(16) | |||
key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), 12) // TODO parameterize. 12 is good today (2016) | |||
if err != nil { | |||
cmn.Exit("Error generating bcrypt key from passphrase: " + err.Error()) | |||
} | |||
key = crypto.Sha256(key) // Get 32 bytes | |||
privKeyBytes := privKey.Bytes() | |||
return saltBytes, crypto.EncryptSymmetric(privKeyBytes, key) | |||
} | |||
func decryptPrivKey(saltBytes []byte, encBytes []byte, passphrase string) (privKey crypto.PrivKey, err error) { | |||
key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), 12) // TODO parameterize. 12 is good today (2016) | |||
if err != nil { | |||
cmn.Exit("Error generating bcrypt key from passphrase: " + err.Error()) | |||
} | |||
key = crypto.Sha256(key) // Get 32 bytes | |||
privKeyBytes, err := crypto.DecryptSymmetric(encBytes, key) | |||
if err != nil { | |||
return privKey, err | |||
} | |||
privKey, err = crypto.PrivKeyFromBytes(privKeyBytes) | |||
return privKey, err | |||
} |
@ -1,177 +0,0 @@ | |||
/* | |||
package filestorage provides a secure on-disk storage of private keys and | |||
metadata. Security is enforced by file and directory permissions, much | |||
like standard ssh key storage. | |||
*/ | |||
package filestorage | |||
import ( | |||
"fmt" | |||
"io/ioutil" | |||
"os" | |||
"path" | |||
"strings" | |||
"github.com/pkg/errors" | |||
crypto "github.com/tendermint/go-crypto" | |||
keys "github.com/tendermint/go-crypto/keys" | |||
) | |||
const ( | |||
BlockType = "Tendermint Light Client" | |||
// PrivExt is the extension for private keys. | |||
PrivExt = "tlc" | |||
// PubExt is the extensions for public keys. | |||
PubExt = "pub" | |||
keyPerm = os.FileMode(0600) | |||
// pubPerm = os.FileMode(0644) | |||
dirPerm = os.FileMode(0700) | |||
) | |||
type FileStore struct { | |||
keyDir string | |||
} | |||
// New creates an instance of file-based key storage with tight permissions | |||
// | |||
// dir should be an absolute path of a directory owner by this user. It will | |||
// be created if it doesn't exist already. | |||
func New(dir string) FileStore { | |||
err := os.MkdirAll(dir, dirPerm) | |||
if err != nil { | |||
panic(err) | |||
} | |||
return FileStore{dir} | |||
} | |||
// assert FileStore satisfies keys.Storage | |||
var _ keys.Storage = FileStore{} | |||
// Put creates two files, one with the public info as json, the other | |||
// with the (encoded) private key as gpg ascii-armor style | |||
func (s FileStore) Put(name string, key []byte, info keys.Info) error { | |||
pub, priv := s.nameToPaths(name) | |||
// write public info | |||
err := writeInfo(pub, info) | |||
if err != nil { | |||
return err | |||
} | |||
// write private info | |||
return write(priv, name, key) | |||
} | |||
// Get loads the info and (encoded) private key from the directory | |||
// It uses `name` to generate the filename, and returns an error if the | |||
// files don't exist or are in the incorrect format | |||
func (s FileStore) Get(name string) ([]byte, keys.Info, error) { | |||
pub, priv := s.nameToPaths(name) | |||
info, err := readInfo(pub) | |||
if err != nil { | |||
return nil, info, err | |||
} | |||
key, _, err := read(priv) | |||
return key, info.Format(), err | |||
} | |||
// List parses the key directory for public info and returns a list of | |||
// Info for all keys located in this directory. | |||
func (s FileStore) List() (keys.Infos, error) { | |||
dir, err := os.Open(s.keyDir) | |||
if err != nil { | |||
return nil, errors.Wrap(err, "List Keys") | |||
} | |||
defer dir.Close() | |||
names, err := dir.Readdirnames(0) | |||
if err != nil { | |||
return nil, errors.Wrap(err, "List Keys") | |||
} | |||
// filter names for .pub ending and load them one by one | |||
// half the files is a good guess for pre-allocating the slice | |||
infos := make([]keys.Info, 0, len(names)/2) | |||
for _, name := range names { | |||
if strings.HasSuffix(name, PubExt) { | |||
p := path.Join(s.keyDir, name) | |||
info, err := readInfo(p) | |||
if err != nil { | |||
return nil, err | |||
} | |||
infos = append(infos, info.Format()) | |||
} | |||
} | |||
return infos, nil | |||
} | |||
// Delete permanently removes the public and private info for the named key | |||
// The calling function should provide some security checks first. | |||
func (s FileStore) Delete(name string) error { | |||
pub, priv := s.nameToPaths(name) | |||
err := os.Remove(priv) | |||
if err != nil { | |||
return errors.Wrap(err, "Deleting Private Key") | |||
} | |||
err = os.Remove(pub) | |||
return errors.Wrap(err, "Deleting Public Key") | |||
} | |||
func (s FileStore) nameToPaths(name string) (pub, priv string) { | |||
privName := fmt.Sprintf("%s.%s", name, PrivExt) | |||
pubName := fmt.Sprintf("%s.%s", name, PubExt) | |||
return path.Join(s.keyDir, pubName), path.Join(s.keyDir, privName) | |||
} | |||
func writeInfo(path string, info keys.Info) error { | |||
return write(path, info.Name, info.PubKey.Bytes()) | |||
} | |||
func readInfo(path string) (info keys.Info, err error) { | |||
var data []byte | |||
data, info.Name, err = read(path) | |||
if err != nil { | |||
return | |||
} | |||
pk, err := crypto.PubKeyFromBytes(data) | |||
info.PubKey = pk | |||
return | |||
} | |||
func read(path string) ([]byte, string, error) { | |||
f, err := os.Open(path) | |||
if err != nil { | |||
return nil, "", errors.Wrap(err, "Reading data") | |||
} | |||
defer f.Close() | |||
d, err := ioutil.ReadAll(f) | |||
if err != nil { | |||
return nil, "", errors.Wrap(err, "Reading data") | |||
} | |||
block, headers, key, err := crypto.DecodeArmor(string(d)) | |||
if err != nil { | |||
return nil, "", errors.Wrap(err, "Invalid Armor") | |||
} | |||
if block != BlockType { | |||
return nil, "", errors.Errorf("Unknown key type: %s", block) | |||
} | |||
return key, headers["name"], nil | |||
} | |||
func write(path, name string, key []byte) error { | |||
f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, keyPerm) | |||
if err != nil { | |||
return errors.Wrap(err, "Writing data") | |||
} | |||
defer f.Close() | |||
headers := map[string]string{"name": name} | |||
text := crypto.EncodeArmor(BlockType, headers, key) | |||
_, err = f.WriteString(text) | |||
return errors.Wrap(err, "Writing data") | |||
} |
@ -1,106 +0,0 @@ | |||
package filestorage | |||
import ( | |||
"io/ioutil" | |||
"os" | |||
"path" | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/require" | |||
crypto "github.com/tendermint/go-crypto" | |||
keys "github.com/tendermint/go-crypto/keys" | |||
) | |||
func TestBasicCRUD(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
dir, err := ioutil.TempDir("", "filestorage-test") | |||
assert.Nil(err) | |||
defer os.RemoveAll(dir) | |||
store := New(dir) | |||
name := "bar" | |||
key := []byte("secret-key-here") | |||
pubkey := crypto.GenPrivKeyEd25519().PubKey() | |||
info := keys.Info{ | |||
Name: name, | |||
PubKey: pubkey.Wrap(), | |||
} | |||
// No data: Get and Delete return nothing | |||
_, _, err = store.Get(name) | |||
assert.NotNil(err) | |||
err = store.Delete(name) | |||
assert.NotNil(err) | |||
// List returns empty list | |||
l, err := store.List() | |||
assert.Nil(err) | |||
assert.Empty(l) | |||
// Putting the key in the store must work | |||
err = store.Put(name, key, info) | |||
assert.Nil(err) | |||
// But a second time is a failure | |||
err = store.Put(name, key, info) | |||
assert.NotNil(err) | |||
// Now, we can get and list properly | |||
k, i, err := store.Get(name) | |||
require.Nil(err, "%+v", err) | |||
assert.Equal(key, k) | |||
assert.Equal(info.Name, i.Name) | |||
assert.Equal(info.PubKey, i.PubKey) | |||
assert.NotEmpty(i.Address) | |||
l, err = store.List() | |||
require.Nil(err, "%+v", err) | |||
assert.Equal(1, len(l)) | |||
assert.Equal(i, l[0]) | |||
// querying a non-existent key fails | |||
_, _, err = store.Get("badname") | |||
assert.NotNil(err) | |||
// We can only delete once | |||
err = store.Delete(name) | |||
assert.Nil(err) | |||
err = store.Delete(name) | |||
assert.NotNil(err) | |||
// and then Get and List don't work | |||
_, _, err = store.Get(name) | |||
assert.NotNil(err) | |||
// List returns empty list | |||
l, err = store.List() | |||
assert.Nil(err) | |||
assert.Empty(l) | |||
} | |||
func TestDirectoryHandling(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
// prepare a temp dir and make sure it is not there | |||
newDir := path.Join(os.TempDir(), "file-test-dir") | |||
_, err := os.Open(newDir) | |||
assert.True(os.IsNotExist(err)) | |||
defer os.RemoveAll(newDir) | |||
// now, check with two levels deep.... | |||
parentDir := path.Join(os.TempDir(), "missing-dir") | |||
nestedDir := path.Join(parentDir, "lots", "of", "levels", "here") | |||
_, err = os.Open(parentDir) | |||
assert.True(os.IsNotExist(err)) | |||
defer os.RemoveAll(parentDir) | |||
// create a new storage, and verify it creates the directory with good permissions | |||
for _, dir := range []string{newDir, nestedDir, newDir} { | |||
New(dir) | |||
d, err := os.Open(dir) | |||
require.Nil(err) | |||
defer d.Close() | |||
stat, err := d.Stat() | |||
require.Nil(err) | |||
assert.Equal(dirPerm, stat.Mode()&os.ModePerm) | |||
} | |||
} |
@ -1,68 +0,0 @@ | |||
/* | |||
package memstorage provides a simple in-memory key store designed for | |||
use in test cases, particularly to isolate them from the filesystem, | |||
concurrency, and cleanup issues. | |||
*/ | |||
package memstorage | |||
import ( | |||
"github.com/pkg/errors" | |||
keys "github.com/tendermint/go-crypto/keys" | |||
) | |||
type data struct { | |||
info keys.Info | |||
key []byte | |||
} | |||
type MemStore map[string]data | |||
// New creates an instance of file-based key storage with tight permissions | |||
func New() MemStore { | |||
return MemStore{} | |||
} | |||
// assert MemStore satisfies keys.Storage | |||
var _ keys.Storage = MemStore{} | |||
// Put adds the given key, returns an error if it another key | |||
// is already stored under this name | |||
func (s MemStore) Put(name string, key []byte, info keys.Info) error { | |||
if _, ok := s[name]; ok { | |||
return errors.Errorf("Key named '%s' already exists", name) | |||
} | |||
s[name] = data{info, key} | |||
return nil | |||
} | |||
// Get returns the key stored under the name, or returns an error if not present | |||
func (s MemStore) Get(name string) ([]byte, keys.Info, error) { | |||
var err error | |||
d, ok := s[name] | |||
if !ok { | |||
err = errors.Errorf("Key named '%s' doesn't exist", name) | |||
} | |||
return d.key, d.info.Format(), err | |||
} | |||
// List returns the public info of all keys in the MemStore in unsorted order | |||
func (s MemStore) List() (keys.Infos, error) { | |||
res := make([]keys.Info, len(s)) | |||
i := 0 | |||
for _, d := range s { | |||
res[i] = d.info.Format() | |||
i++ | |||
} | |||
return res, nil | |||
} | |||
// Delete removes the named key from the MemStore, raising an error if it | |||
// wasn't present yet. | |||
func (s MemStore) Delete(name string) error { | |||
_, ok := s[name] | |||
if !ok { | |||
return errors.Errorf("Key named '%s' doesn't exist", name) | |||
} | |||
delete(s, name) | |||
return nil | |||
} |
@ -1,69 +0,0 @@ | |||
package memstorage | |||
import ( | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
crypto "github.com/tendermint/go-crypto" | |||
keys "github.com/tendermint/go-crypto/keys" | |||
) | |||
func TestBasicCRUD(t *testing.T) { | |||
assert := assert.New(t) | |||
store := New() | |||
name := "foo" | |||
key := []byte("secret-key-here") | |||
pubkey := crypto.GenPrivKeyEd25519().PubKey() | |||
info := keys.Info{ | |||
Name: name, | |||
PubKey: pubkey, | |||
} | |||
// No data: Get and Delete return nothing | |||
_, _, err := store.Get(name) | |||
assert.NotNil(err) | |||
err = store.Delete(name) | |||
assert.NotNil(err) | |||
// List returns empty list | |||
l, err := store.List() | |||
assert.Nil(err) | |||
assert.Empty(l) | |||
// Putting the key in the store must work | |||
err = store.Put(name, key, info) | |||
assert.Nil(err) | |||
// But a second time is a failure | |||
err = store.Put(name, key, info) | |||
assert.NotNil(err) | |||
// Now, we can get and list properly | |||
k, i, err := store.Get(name) | |||
assert.Nil(err) | |||
assert.Equal(key, k) | |||
assert.Equal(info.Name, i.Name) | |||
assert.Equal(info.PubKey, i.PubKey) | |||
assert.NotEmpty(i.Address) | |||
l, err = store.List() | |||
assert.Nil(err) | |||
assert.Equal(1, len(l)) | |||
assert.Equal(i, l[0]) | |||
// querying a non-existent key fails | |||
_, _, err = store.Get("badname") | |||
assert.NotNil(err) | |||
// We can only delete once | |||
err = store.Delete(name) | |||
assert.Nil(err) | |||
err = store.Delete(name) | |||
assert.NotNil(err) | |||
// and then Get and List don't work | |||
_, _, err = store.Get(name) | |||
assert.NotNil(err) | |||
// List returns empty list | |||
l, err = store.List() | |||
assert.Nil(err) | |||
assert.Empty(l) | |||
} |
@ -1,134 +1,48 @@ | |||
package keys | |||
import ( | |||
"fmt" | |||
"sort" | |||
wire "github.com/tendermint/go-wire" | |||
crypto "github.com/tendermint/go-crypto" | |||
wire "github.com/tendermint/go-wire" | |||
data "github.com/tendermint/go-wire/data" | |||
) | |||
// Storage has many implementation, based on security and sharing requirements | |||
// like disk-backed, mem-backed, vault, db, etc. | |||
type Storage interface { | |||
Put(name string, key []byte, info Info) error | |||
Get(name string) (key []byte, info Info, err error) | |||
List() (Infos, error) | |||
Delete(name string) error | |||
} | |||
// Info is the public information about a key | |||
type Info struct { | |||
Name string `json:"name"` | |||
Address data.Bytes `json:"address"` | |||
PubKey crypto.PubKey `json:"pubkey"` | |||
Name string `json:"name"` | |||
PubKey crypto.PubKey `json:"pubkey"` | |||
} | |||
func (i *Info) Format() Info { | |||
if !i.PubKey.Empty() { | |||
i.Address = i.PubKey.Address() | |||
} | |||
return *i | |||
// Address is a helper function to calculate the address from the pubkey | |||
func (i Info) Address() []byte { | |||
return i.PubKey.Address() | |||
} | |||
// Infos is a wrapper to allows alphabetical sorting of the keys | |||
type Infos []Info | |||
func (k Infos) Len() int { return len(k) } | |||
func (k Infos) Less(i, j int) bool { return k[i].Name < k[j].Name } | |||
func (k Infos) Swap(i, j int) { k[i], k[j] = k[j], k[i] } | |||
func (k Infos) Sort() { | |||
if k != nil { | |||
sort.Sort(k) | |||
} | |||
func (i Info) bytes() []byte { | |||
return wire.BinaryBytes(i) | |||
} | |||
// Signable represents any transaction we wish to send to tendermint core | |||
// These methods allow us to sign arbitrary Tx with the KeyStore | |||
type Signable interface { | |||
// SignBytes is the immutable data, which needs to be signed | |||
SignBytes() []byte | |||
// Sign will add a signature and pubkey. | |||
// | |||
// Depending on the Signable, one may be able to call this multiple times for multisig | |||
// Returns error if called with invalid data or too many times | |||
Sign(pubkey crypto.PubKey, sig crypto.Signature) error | |||
// Signers will return the public key(s) that signed if the signature | |||
// is valid, or an error if there is any issue with the signature, | |||
// including if there are no signatures | |||
Signers() ([]crypto.PubKey, error) | |||
// TxBytes returns the transaction data as well as all signatures | |||
// It should return an error if Sign was never called | |||
TxBytes() ([]byte, error) | |||
func readInfo(bs []byte) (info Info, err error) { | |||
err = wire.ReadBinaryBytes(bs, &info) | |||
return | |||
} | |||
// Signer allows one to use a keystore to sign transactions | |||
type Signer interface { | |||
Sign(name, passphrase string, tx Signable) error | |||
func info(name string, privKey crypto.PrivKey) Info { | |||
return Info{ | |||
Name: name, | |||
PubKey: privKey.PubKey(), | |||
} | |||
} | |||
// Manager allows simple CRUD on a keystore, as an aid to signing | |||
type Manager interface { | |||
Signer | |||
// Create also returns a seed phrase for cold-storage | |||
Create(name, passphrase, algo string) (Info, string, error) | |||
// Recover takes a seedphrase and loads in the private key | |||
// Keybase allows simple CRUD on a keystore, as an aid to signing | |||
type Keybase interface { | |||
// Sign some bytes | |||
Sign(name, passphrase string, msg []byte) (crypto.Signature, crypto.PubKey, error) | |||
// Create a new keypair | |||
Create(name, passphrase, algo string) (_ Info, seedphrase string, _ error) | |||
// Recover takes a seedphrase and loads in the key | |||
Recover(name, passphrase, seedphrase string) (Info, error) | |||
List() (Infos, error) | |||
List() ([]Info, error) | |||
Get(name string) (Info, error) | |||
Update(name, oldpass, newpass string) error | |||
Delete(name, passphrase string) error | |||
} | |||
/**** MockSignable allows us to view data ***/ | |||
// MockSignable lets us wrap arbitrary data with a go-crypto signature | |||
type MockSignable struct { | |||
Data []byte | |||
PubKey crypto.PubKey | |||
Signature crypto.Signature | |||
} | |||
var _ Signable = &MockSignable{} | |||
// NewMockSignable sets the data to sign | |||
func NewMockSignable(data []byte) *MockSignable { | |||
return &MockSignable{Data: data} | |||
} | |||
// TxBytes returns the full data with signatures | |||
func (s *MockSignable) TxBytes() ([]byte, error) { | |||
return wire.BinaryBytes(s), nil | |||
} | |||
// SignBytes returns the original data passed into `NewSig` | |||
func (s *MockSignable) SignBytes() []byte { | |||
return s.Data | |||
} | |||
// Sign will add a signature and pubkey. | |||
// | |||
// Depending on the Signable, one may be able to call this multiple times for multisig | |||
// Returns error if called with invalid data or too many times | |||
func (s *MockSignable) Sign(pubkey crypto.PubKey, sig crypto.Signature) error { | |||
s.PubKey = pubkey | |||
s.Signature = sig | |||
return nil | |||
} | |||
// Signers will return the public key(s) that signed if the signature | |||
// is valid, or an error if there is any issue with the signature, | |||
// including if there are no signatures | |||
func (s *MockSignable) Signers() ([]crypto.PubKey, error) { | |||
if s.PubKey.Empty() { | |||
return nil, fmt.Errorf("no signers") | |||
} | |||
if !s.PubKey.VerifyBytes(s.SignBytes(), s.Signature) { | |||
return nil, fmt.Errorf("invalid signature") | |||
} | |||
return []crypto.PubKey{s.PubKey}, nil | |||
} |