diff --git a/Makefile b/Makefile index f775704a9..c1974c410 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ REPO:=github.com/tendermint/go-crypto all: get_vendor_deps metalinter_test test test: - go test `glide novendor` + go test -p 1 `glide novendor` get_vendor_deps: ensure_tools @rm -rf vendor/ diff --git a/glide.lock b/glide.lock index 1c23d8bea..29124c3db 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: c0a2db1b80c6b1b8aab31c526ce43e22e49b23c893c78b8fdb8546aa2e7b7cc6 -updated: 2017-09-22T10:21:34.220901552-04:00 +hash: a2243bfd21937edf660778300855e7cb72185164641cb278dbf0c220e8a0f60a +updated: 2017-10-23T17:21:02.40831023+02:00 imports: - name: github.com/bgentry/speakeasy version: 4aabc24848ce5fd31929f7d1e4ea74d3709c14cd @@ -11,16 +11,16 @@ imports: - chaincfg/chainhash - wire - name: github.com/btcsuite/btcutil - version: 86346b5a958c0cf94186b87855469ae991be501c + version: 66871daeb12123ece012a9628d2798d01195c4b3 subpackages: - base58 - hdkeychain - name: github.com/btcsuite/fastsha256 version: 637e656429416087660c84436a2a035d69d54e2e -- name: github.com/btcsuite/golangcrypto - version: 53f62d9b43e87a6c56975cf862af7edf33a8d0df - subpackages: - - ripemd160 +- name: github.com/ethanfrey/ledger + version: 5e432577be582bd18a3b4a9cd75dae7a317ade36 +- name: github.com/flynn/hid + version: ed06a31c6245d4552e8dbba7e32e5b010b875d65 - name: github.com/go-kit/kit version: d67bb4c202e3b91377d1079b110a6c9ce23ab2f8 subpackages: @@ -44,7 +44,7 @@ imports: - name: github.com/gorilla/mux version: bcd8bc72b08df0f70df986b97f95590779502d31 - name: github.com/howeyc/crc16 - version: 96a97a1abb579c7ff1a8ffa77f2e72d1c314b57f + version: 58da63c846043d0bea709c8d47039df06577d6d9 - name: github.com/kr/logfmt version: b84e30acd515aadc4b783ad4ff83aff3299bdfe0 - name: github.com/pkg/errors @@ -59,12 +59,12 @@ imports: - edwards25519 - extra25519 - name: github.com/tendermint/go-wire - version: 5f88da3dbc1a72844e6dfaf274ce87f851d488eb + version: 55ae61f1fc83cfaa57ab7d54250d7a1a2be0b83c subpackages: - data - data/base58 - name: github.com/tendermint/tmlibs - version: bffe6744ec277d60f707ab442e25513617842f8e + version: 8e5266a9ef2527e68a1571f932db8228a331b556 subpackages: - common - log @@ -81,16 +81,14 @@ imports: - ripemd160 - salsa20/salsa - name: gopkg.in/go-playground/validator.v9 - version: d529ee1b0f30352444f507cc6cdac96bfd12decc + version: 6d8c18553ea1ac493d049edd6f102f52e618f085 testImports: +- name: github.com/cmars/basen + version: fe3947df716ebfda9847eb1b9a48f9592e06478c - name: github.com/davecgh/go-spew version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9 subpackages: - spew -- name: github.com/FactomProject/basen - version: fe3947df716ebfda9847eb1b9a48f9592e06478c -- name: github.com/FactomProject/btcutilecc - version: d3a63a5752ecf3fbc06bd97365da752111c263df - name: github.com/mndrix/btcutil version: d3a63a5752ecf3fbc06bd97365da752111c263df - name: github.com/pmezard/go-difflib @@ -103,6 +101,6 @@ testImports: - assert - require - name: github.com/tyler-smith/go-bip32 - version: 2c9cfd17756470a0b7c3e4b7954bae7d11035504 + version: eb790af526c30f23a7c8b00a48e342f9d0bd6386 - name: github.com/tyler-smith/go-bip39 version: 8e7a99b3e716f36d3b080a9a70f9eb45abe4edcc diff --git a/glide.yaml b/glide.yaml index a99d3b63f..9f20ddefc 100644 --- a/glide.yaml +++ b/glide.yaml @@ -30,6 +30,7 @@ import: - package: github.com/spf13/viper - package: gopkg.in/go-playground/validator.v9 - package: github.com/howeyc/crc16 +- package: github.com/ethanfrey/ledger testImport: - package: github.com/mndrix/btcutil - package: github.com/stretchr/testify diff --git a/keys/cryptostore/encoder.go b/keys/cryptostore/encoder.go index 251543b60..31f49c3f7 100644 --- a/keys/cryptostore/encoder.go +++ b/keys/cryptostore/encoder.go @@ -43,7 +43,8 @@ func (e secretbox) Decrypt(saltBytes []byte, encBytes []byte, passphrase string) privKeyBytes := encBytes // NOTE: Some keys weren't encrypted with a passphrase and hence we have the conditional if passphrase != "" { - key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), 14) // TODO parameterize. 14 is good today (2016) + var key []byte + key, err = bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), 14) // TODO parameterize. 14 is good today (2016) if err != nil { return crypto.PrivKey{}, errors.Wrap(err, "Invalid Passphrase") } @@ -55,7 +56,7 @@ func (e secretbox) Decrypt(saltBytes []byte, encBytes []byte, passphrase string) } privKey, err = crypto.PrivKeyFromBytes(privKeyBytes) if err != nil { - return crypto.PrivKey{}, errors.Wrap(err, "Couldn't get privKey from bytes") + return crypto.PrivKey{}, errors.Wrap(err, "Private Key") } return privKey, nil } diff --git a/keys/cryptostore/encoder_test.go b/keys/cryptostore/encoder_test.go index ce1118d20..614286a19 100644 --- a/keys/cryptostore/encoder_test.go +++ b/keys/cryptostore/encoder_test.go @@ -15,8 +15,10 @@ func TestNoopEncoder(t *testing.T) { assert, require := assert.New(t), require.New(t) noop := cryptostore.Noop - key := cryptostore.GenEd25519.Generate(cmn.RandBytes(16)) - key2 := cryptostore.GenSecp256k1.Generate(cmn.RandBytes(16)) + 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) @@ -43,7 +45,8 @@ func TestSecretBox(t *testing.T) { assert, require := assert.New(t), require.New(t) enc := cryptostore.SecretBox - key := cryptostore.GenEd25519.Generate(cmn.RandBytes(16)) + key, err := cryptostore.GenEd25519.Generate(cmn.RandBytes(16)) + require.NoError(err) pass := "some-special-secret" s, b, err := enc.Encrypt(key, pass) @@ -65,7 +68,8 @@ func TestSecretBoxNoPass(t *testing.T) { assert, require := assert.New(t), require.New(t) enc := cryptostore.SecretBox - key := cryptostore.GenEd25519.Generate(cmn.RandBytes(16)) + key, rerr := cryptostore.GenEd25519.Generate(cmn.RandBytes(16)) + require.NoError(rerr) cases := []struct { encode string @@ -95,7 +99,7 @@ func TestSecretBoxNoPass(t *testing.T) { // now let's make sure raw bytes also work... b := key.Bytes() - pk, err := enc.Decrypt(nil, b, "") - require.Nil(err, "%+v", err) + pk, rerr := enc.Decrypt(nil, b, "") + require.NoError(rerr) assert.Equal(key, pk) } diff --git a/keys/cryptostore/generator.go b/keys/cryptostore/generator.go index 1f162ec08..65cc8e58b 100644 --- a/keys/cryptostore/generator.go +++ b/keys/cryptostore/generator.go @@ -4,6 +4,7 @@ import ( "github.com/pkg/errors" crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/go-crypto/nano" ) var ( @@ -11,46 +12,78 @@ var ( 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 + Generate(secret []byte) (crypto.PrivKey, error) } // GenFunc is a helper to transform a function into a Generator -type GenFunc func(secret []byte) crypto.PrivKey +type GenFunc func(secret []byte) (crypto.PrivKey, error) -func (f GenFunc) Generate(secret []byte) crypto.PrivKey { +func (f GenFunc) Generate(secret []byte) (crypto.PrivKey, error) { return f(secret) } -func genEd25519(secret []byte) crypto.PrivKey { - return crypto.GenPrivKeyEd25519FromSecret(secret).Wrap() +func genEd25519(secret []byte) (crypto.PrivKey, error) { + key := crypto.GenPrivKeyEd25519FromSecret(secret).Wrap() + return key, nil } -func genSecp256(secret []byte) crypto.PrivKey { - return crypto.GenPrivKeySecp256k1FromSecret(secret).Wrap() +func genSecp256(secret []byte) (crypto.PrivKey, error) { + key := crypto.GenPrivKeySecp256k1FromSecret(secret).Wrap() + return key, nil } -func getGenerator(algo string) (Generator, error) { +// 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, nil + return GenEd25519 case crypto.NameSecp256k1: - return GenSecp256k1, nil + return GenSecp256k1 + case nano.NameLedgerEd25519: + return GenLedgerEd25519 default: - return nil, errors.Errorf("Cannot generate keys for algorithm: %s", algo) + return genInvalidAlgo{algo} } } -func getGeneratorByType(typ byte) (Generator, error) { +func getGeneratorByType(typ byte) Generator { switch typ { case crypto.TypeEd25519: - return GenEd25519, nil + return GenEd25519 case crypto.TypeSecp256k1: - return GenSecp256k1, nil + return GenSecp256k1 + case nano.TypeLedgerEd25519: + return GenLedgerEd25519 default: - return nil, errors.Errorf("Cannot generate keys for algorithm: %X", typ) + return genInvalidByte{typ} } } diff --git a/keys/cryptostore/holder.go b/keys/cryptostore/holder.go index cb8a2e149..923190c1b 100644 --- a/keys/cryptostore/holder.go +++ b/keys/cryptostore/holder.go @@ -33,14 +33,15 @@ var _ keys.Manager = Manager{} // // algo must be a supported go-crypto algorithm: ed25519, secp256k1 func (s Manager) Create(name, passphrase, algo string) (keys.Info, string, error) { - gen, err := getGenerator(algo) + // 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 } - // 128-bits are the all the randomness we can make use of - secret := crypto.CRandBytes(16) - key := gen.Generate(secret) err = s.es.Put(name, passphrase, key) if err != nil { return keys.Info{}, "", err @@ -74,11 +75,11 @@ func (s Manager) Recover(name, passphrase, seedphrase string) (keys.Info, error) l := len(secret) secret, typ := secret[:l-1], secret[l-1] - gen, err := getGeneratorByType(typ) + gen := getGeneratorByType(typ) + key, err := gen.Generate(secret) if err != nil { return keys.Info{}, err } - key := gen.Generate(secret) // d00d, it worked! create the bugger.... err = s.es.Put(name, passphrase, key) diff --git a/keys/cryptostore/holder_test.go b/keys/cryptostore/holder_test.go index a8fc90989..3709cc55d 100644 --- a/keys/cryptostore/holder_test.go +++ b/keys/cryptostore/holder_test.go @@ -1,6 +1,9 @@ package cryptostore_test import ( + "bytes" + "fmt" + "os" "testing" "github.com/stretchr/testify/assert" @@ -12,6 +15,7 @@ import ( "github.com/tendermint/go-crypto/keys" "github.com/tendermint/go-crypto/keys/cryptostore" "github.com/tendermint/go-crypto/keys/storage/memstorage" + "github.com/tendermint/go-crypto/nano" ) // TestKeyManagement makes sure we can manipulate these keys well @@ -84,65 +88,123 @@ func TestKeyManagement(t *testing.T) { // TestSignVerify does some detailed checks on how we sign and validate // signatures -// func TestSignVerify(t *testing.T) { -// assert, require := assert.New(t), require.New(t) +func TestSignVerify(t *testing.T) { + assert, require := assert.New(t), require.New(t) -// // make the storage with reasonable defaults -// cstore := cryptostore.New( -// cryptostore.GenSecp256k1, -// cryptostore.SecretBox, -// memstorage.New(), -// ) + // make the storage with reasonable defaults + cstore := cryptostore.New( + cryptostore.SecretBox, + memstorage.New(), + keys.MustLoadCodec("english"), + ) + algo := crypto.NameSecp256k1 -// n1, n2 := "some dude", "a dudette" -// p1, p2 := "1234", "foobar" + n1, n2 := "some dude", "a dudette" + p1, p2 := "1234", "foobar" -// // create two users and get their info -// err := cstore.Create(n1, p1) -// require.Nil(err) -// i1, err := cstore.Get(n1) -// require.Nil(err) + // create two users and get their info + i1, _, err := cstore.Create(n1, p1, algo) + require.Nil(err) -// err = cstore.Create(n2, p2) -// require.Nil(err) -// i2, err := cstore.Get(n2) -// require.Nil(err) + i2, _, err := cstore.Create(n2, p2, algo) + require.Nil(err) -// // let's try to sign some messages -// d1 := []byte("my first message") -// d2 := []byte("some other important info!") + // let's try to sign some messages + d1 := []byte("my first message") + d2 := []byte("some other important info!") -// // try signing both data with both keys... -// s11, err := cstore.Signature(n1, p1, d1) -// require.Nil(err) -// s12, err := cstore.Signature(n1, p1, d2) -// require.Nil(err) -// s21, err := cstore.Signature(n2, p2, d1) -// require.Nil(err) -// s22, err := cstore.Signature(n2, p2, d2) -// require.Nil(err) + // try signing both data with both keys... + s11 := keys.NewMockSignable(d1) + err = cstore.Sign(n1, p1, s11) + require.Nil(err) + s12 := keys.NewMockSignable(d2) + err = cstore.Sign(n1, p1, s12) + require.Nil(err) + s21 := keys.NewMockSignable(d1) + err = cstore.Sign(n2, p2, s21) + require.Nil(err) + s22 := keys.NewMockSignable(d2) + err = cstore.Sign(n2, p2, s22) + require.Nil(err) -// // let's try to validate and make sure it only works when everything is proper -// keys := [][]byte{i1.PubKey, i2.PubKey} -// data := [][]byte{d1, d2} -// sigs := [][]byte{s11, s12, s21, s22} + // let's try to validate and make sure it only works when everything is proper + cases := []struct { + key crypto.PubKey + data []byte + sig crypto.Signature + valid bool + }{ + // proper matches + {i1.PubKey, d1, s11.Signature, true}, + // change data, pubkey, or signature leads to fail + {i1.PubKey, d2, s11.Signature, false}, + {i2.PubKey, d1, s11.Signature, false}, + {i1.PubKey, d1, s21.Signature, false}, + // make sure other successes + {i1.PubKey, d2, s12.Signature, true}, + {i2.PubKey, d1, s21.Signature, true}, + {i2.PubKey, d2, s22.Signature, true}, + } -// // loop over keys and data -// for k := 0; k < 2; k++ { -// for d := 0; d < 2; d++ { -// // make sure only the proper sig works -// good := 2*k + d -// for s := 0; s < 4; s++ { -// err = cstore.Verify(data[d], sigs[s], keys[k]) -// if s == good { -// assert.Nil(err, "%+v", err) -// } else { -// assert.NotNil(err) -// } -// } -// } -// } -// } + for i, tc := range cases { + valid := tc.key.VerifyBytes(tc.data, tc.sig) + assert.Equal(tc.valid, valid, "%d", i) + } +} + +// TestSignWithLedger makes sure we have ledger compatibility with +// the crypto store. +// +// This test will only succeed with a ledger attached to the computer +// and the cosmos app open +func TestSignWithLedger(t *testing.T) { + assert, require := assert.New(t), require.New(t) + if os.Getenv("WITH_LEDGER") == "" { + t.Skip("Set WITH_LEDGER to run code on real ledger") + } + + // make the storage with reasonable defaults + cstore := cryptostore.New( + cryptostore.SecretBox, + memstorage.New(), + keys.MustLoadCodec("english"), + ) + n := "nano-s" + p := "hard2hack" + + // create a nano user + c, _, err := cstore.Create(n, p, nano.NameLedgerEd25519) + require.Nil(err, "%+v", err) + assert.Equal(c.Name, n) + _, ok := c.PubKey.Unwrap().(nano.PubKeyLedgerEd25519) + require.True(ok) + + // make sure we can get it back + info, err := cstore.Get(n) + require.Nil(err, "%+v", err) + assert.Equal(info.Name, n) + key := info.PubKey + require.False(key.Empty()) + require.True(key.Equals(c.PubKey)) + + // let's try to sign some messages + d1 := []byte("welcome to cosmos") + d2 := []byte("please turn on the app") + + // try signing both data with the ledger... + s1 := keys.NewMockSignable(d1) + err = cstore.Sign(n, p, s1) + require.Nil(err) + s2 := keys.NewMockSignable(d2) + err = cstore.Sign(n, p, s2) + require.Nil(err) + + // now, let's check those signatures work + assert.True(key.VerifyBytes(d1, s1.Signature)) + assert.True(key.VerifyBytes(d2, s2.Signature)) + // and mismatched signatures don't + assert.False(key.VerifyBytes(d1, s2.Signature)) +} func assertPassword(assert *assert.Assertions, cstore cryptostore.Manager, name, pass, badpass string) { err := cstore.Update(name, badpass, pass) @@ -162,13 +224,15 @@ func TestImportUnencrypted(t *testing.T) { keys.MustLoadCodec("english"), ) - key := cryptostore.GenEd25519.Generate(cmn.RandBytes(16)) + key, err := cryptostore.GenEd25519.Generate(cmn.RandBytes(16)) + require.NoError(err) + addr := key.PubKey().Address() name := "john" pass := "top-secret" // import raw bytes - err := cstore.Import(name, pass, "", nil, key.Bytes()) + err = cstore.Import(name, pass, "", nil, key.Bytes()) require.Nil(err, "%+v", err) // make sure the address matches @@ -256,45 +320,59 @@ func TestSeedPhrase(t *testing.T) { assert.Equal(info.PubKey, newInfo.PubKey) } -// func ExampleStore() { -// // Select the encryption and storage for your cryptostore -// cstore := cryptostore.New( -// cryptostore.GenEd25519, -// cryptostore.SecretBox, -// // Note: use filestorage.New(dir) for real data -// memstorage.New(), -// ) +func ExampleNew() { + // Select the encryption and storage for your cryptostore + cstore := cryptostore.New( + cryptostore.SecretBox, + // Note: use filestorage.New(dir) for real data + memstorage.New(), + keys.MustLoadCodec("english"), + ) + ed := crypto.NameEd25519 + sec := crypto.NameSecp256k1 -// // Add keys and see they return in alphabetical order -// cstore.Create("Bob", "friend") -// cstore.Create("Alice", "secret") -// cstore.Create("Carl", "mitm") -// info, _ := cstore.List() -// for _, i := range info { -// fmt.Println(i.Name) -// } + // Add keys and see they return in alphabetical order + bob, _, err := cstore.Create("Bob", "friend", ed) + if err != nil { + // this should never happen + fmt.Println(err) + } else { + // return info here just like in List + fmt.Println(bob.Name) + } + cstore.Create("Alice", "secret", sec) + cstore.Create("Carl", "mitm", ed) + info, _ := cstore.List() + for _, i := range info { + fmt.Println(i.Name) + } -// // We need to use passphrase to generate a signature -// tx := mock.NewSig([]byte("deadbeef")) -// err := cstore.Sign("Bob", "friend", tx) -// if err != nil { -// fmt.Println("don't accept real passphrase") -// } + // We need to use passphrase to generate a signature + tx := keys.NewMockSignable([]byte("deadbeef")) + err = cstore.Sign("Bob", "friend", tx) + if err != nil { + fmt.Println("don't accept real passphrase") + } -// // and we can validate the signature with publically available info -// binfo, _ := cstore.Get("Bob") -// sigs, err := tx.Signers() -// if err != nil { -// fmt.Println("badly signed") -// } else if bytes.Equal(sigs[0].Bytes(), binfo.PubKey.Bytes()) { -// fmt.Println("signed by Bob") -// } else { -// fmt.Println("signed by someone else") -// } + // and we can validate the signature with publically available info + binfo, _ := cstore.Get("Bob") + if !binfo.PubKey.Equals(bob.PubKey) { + fmt.Println("Get and Create return different keys") + } -// // Output: -// // Alice -// // Bob -// // Carl -// // signed by Bob -// } + sigs, err := tx.Signers() + if err != nil { + fmt.Println("badly signed") + } else if bytes.Equal(sigs[0].Bytes(), binfo.PubKey.Bytes()) { + fmt.Println("signed by Bob") + } else { + fmt.Println("signed by someone else") + } + + // Output: + // Bob + // Alice + // Bob + // Carl + // signed by Bob +} diff --git a/keys/cryptostore/storage_test.go b/keys/cryptostore/storage_test.go index 468435143..23931c294 100644 --- a/keys/cryptostore/storage_test.go +++ b/keys/cryptostore/storage_test.go @@ -14,7 +14,10 @@ import ( func TestSortKeys(t *testing.T) { assert := assert.New(t) - gen := func() crypto.PrivKey { return GenEd25519.Generate(cmn.RandBytes(16)) } + gen := func() crypto.PrivKey { + key, _ := GenEd25519.Generate(cmn.RandBytes(16)) + return key + } assert.NotEqual(gen(), gen()) // alphabetical order is n3, n1, n2 diff --git a/keys/server/README.md b/keys/server/README.md deleted file mode 100644 index 032cf574e..000000000 --- a/keys/server/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Proxy Server - -This package provides all the functionality for a local http server, providing access to key management functionality (creating, listing, updating, and deleting keys). This is a nice building block for larger apps, and the HTTP handlers here can be embedded in a larger server that does nice things like signing transactions and posting them to a tendermint chain (which requires domain-knowledge of the transactions types and out of scope of this generic app). - -## Key Management - -We expose a number of methods for safely managing your keychain. If you are embedding this in a larger server, you will typically want to mount all these paths under `/keys`. - -* `POST /` - provide a name and passphrase and create a brand new key -* `GET /` - get a list of all available key names, along with their public key and address -* `GET /{name}` - get public key and address for this named key -* `PUT /{name}` - update the passphrase for the given key. requires you to correctly provide the current passphrase, as well as a new one. -* `DELETE /{name}` - permanently delete this private key. requires you to correctly provide the current passphrase diff --git a/keys/server/helpers.go b/keys/server/helpers.go deleted file mode 100644 index 3fb947316..000000000 --- a/keys/server/helpers.go +++ /dev/null @@ -1,59 +0,0 @@ -/* -package server provides http handlers to construct a server server -for key management, transaction signing, and query validation. - -Please read the README and godoc to see how to -configure the server for your application. -*/ - -package server - -import ( - "encoding/json" - "io/ioutil" - "net/http" - - "github.com/tendermint/go-crypto/keys/server/types" - data "github.com/tendermint/go-wire/data" - - "github.com/pkg/errors" -) - -func readRequest(r *http.Request, o interface{}) error { - defer r.Body.Close() - data, err := ioutil.ReadAll(r.Body) - if err != nil { - return errors.Wrap(err, "Read Request") - } - err = json.Unmarshal(data, o) - if err != nil { - return errors.Wrap(err, "Parse") - } - return validate(o) -} - -// most errors are bad input, so 406... do better.... -func writeError(w http.ResponseWriter, err error) { - // fmt.Printf("Error: %+v\n", err) - res := types.ErrorResponse{ - Code: 406, - Error: err.Error(), - } - writeCode(w, &res, 406) -} - -func writeCode(w http.ResponseWriter, o interface{}, code int) { - // two space indent to make it easier to read - data, err := data.ToJSON(o) - if err != nil { - writeError(w, err) - } else { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(code) - w.Write(data) - } -} - -func writeSuccess(w http.ResponseWriter, o interface{}) { - writeCode(w, o, 200) -} diff --git a/keys/server/keys.go b/keys/server/keys.go deleted file mode 100644 index 80852802e..000000000 --- a/keys/server/keys.go +++ /dev/null @@ -1,128 +0,0 @@ -package server - -import ( - "errors" - "net/http" - - "github.com/gorilla/mux" - keys "github.com/tendermint/go-crypto/keys" - "github.com/tendermint/go-crypto/keys/server/types" -) - -type Keys struct { - manager keys.Manager - algo string -} - -func New(manager keys.Manager, algo string) Keys { - return Keys{ - manager: manager, - algo: algo, - } -} - -func (k Keys) GenerateKey(w http.ResponseWriter, r *http.Request) { - req := types.CreateKeyRequest{ - Algo: k.algo, // default key type from cli - } - err := readRequest(r, &req) - if err != nil { - writeError(w, err) - return - } - - key, seed, err := k.manager.Create(req.Name, req.Passphrase, req.Algo) - if err != nil { - writeError(w, err) - return - } - - res := types.CreateKeyResponse{key, seed} - writeSuccess(w, &res) -} - -func (k Keys) GetKey(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - name := vars["name"] - key, err := k.manager.Get(name) - if err != nil { - writeError(w, err) - return - } - writeSuccess(w, &key) -} - -func (k Keys) ListKeys(w http.ResponseWriter, r *http.Request) { - - keys, err := k.manager.List() - if err != nil { - writeError(w, err) - return - } - writeSuccess(w, keys) -} - -func (k Keys) UpdateKey(w http.ResponseWriter, r *http.Request) { - req := types.UpdateKeyRequest{} - err := readRequest(r, &req) - if err != nil { - writeError(w, err) - return - } - - vars := mux.Vars(r) - name := vars["name"] - if name != req.Name { - writeError(w, errors.New("path and json key names don't match")) - return - } - - err = k.manager.Update(req.Name, req.OldPass, req.NewPass) - if err != nil { - writeError(w, err) - return - } - - key, err := k.manager.Get(req.Name) - if err != nil { - writeError(w, err) - return - } - writeSuccess(w, &key) -} - -func (k Keys) DeleteKey(w http.ResponseWriter, r *http.Request) { - req := types.DeleteKeyRequest{} - err := readRequest(r, &req) - if err != nil { - writeError(w, err) - return - } - - vars := mux.Vars(r) - name := vars["name"] - if name != req.Name { - writeError(w, errors.New("path and json key names don't match")) - return - } - - err = k.manager.Delete(req.Name, req.Passphrase) - if err != nil { - writeError(w, err) - return - } - - // not really an error, but something generic - resp := types.ErrorResponse{ - Success: true, - } - writeSuccess(w, &resp) -} - -func (k Keys) Register(r *mux.Router) { - r.HandleFunc("/", k.GenerateKey).Methods("POST") - r.HandleFunc("/", k.ListKeys).Methods("GET") - r.HandleFunc("/{name}", k.GetKey).Methods("GET") - r.HandleFunc("/{name}", k.UpdateKey).Methods("POST", "PUT") - r.HandleFunc("/{name}", k.DeleteKey).Methods("DELETE") -} diff --git a/keys/server/keys_test.go b/keys/server/keys_test.go deleted file mode 100644 index 2aa17753c..000000000 --- a/keys/server/keys_test.go +++ /dev/null @@ -1,193 +0,0 @@ -package server_test - -import ( - "bytes" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gorilla/mux" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - keys "github.com/tendermint/go-crypto/keys" - "github.com/tendermint/go-crypto/keys/cryptostore" - "github.com/tendermint/go-crypto/keys/server" - "github.com/tendermint/go-crypto/keys/server/types" - "github.com/tendermint/go-crypto/keys/storage/memstorage" -) - -func TestKeyServer(t *testing.T) { - assert, require := assert.New(t), require.New(t) - r := setupServer() - - // let's abstract this out a bit.... - keys, code, err := listKeys(r) - require.Nil(err) - require.Equal(http.StatusOK, code) - assert.Equal(0, len(keys)) - - algo := "ed25519" - n1, n2 := "personal", "business" - p0, p1, p2 := "1234", "over10chars...", "really-secure!@#$" - - // this fails for validation - _, code, err = createKey(r, n1, p0, algo) - require.Nil(err, "%+v", err) - require.NotEqual(http.StatusOK, code) - - // new password better - key, code, err := createKey(r, n1, p1, algo) - require.Nil(err, "%+v", err) - require.Equal(http.StatusOK, code) - require.Equal(n1, key.Key.Name) - require.NotEmpty(n1, key.Seed) - - // the other one works - key2, code, err := createKey(r, n2, p2, algo) - require.Nil(err, "%+v", err) - require.Equal(http.StatusOK, code) - require.Equal(key2.Key.Name, n2) - require.NotEmpty(n2, key.Seed) - - // let's abstract this out a bit.... - keys, code, err = listKeys(r) - require.Nil(err) - require.Equal(http.StatusOK, code) - if assert.Equal(2, len(keys)) { - // in alphabetical order - assert.Equal(keys[0].Name, n2) - assert.Equal(keys[1].Name, n1) - } - - // get works - k, code, err := getKey(r, n1) - require.Nil(err, "%+v", err) - require.Equal(http.StatusOK, code) - assert.Equal(n1, k.Name) - assert.NotNil(k.Address) - assert.Equal(key.Key.Address, k.Address) - - // delete with proper key - _, code, err = deleteKey(r, n1, p1) - require.Nil(err, "%+v", err) - require.Equal(http.StatusOK, code) - - // after delete, get and list different - _, code, err = getKey(r, n1) - require.Nil(err, "%+v", err) - require.NotEqual(http.StatusOK, code) - keys, code, err = listKeys(r) - require.Nil(err, "%+v", err) - require.Equal(http.StatusOK, code) - if assert.Equal(1, len(keys)) { - assert.Equal(keys[0].Name, n2) - } - -} - -func setupServer() http.Handler { - // make the storage with reasonable defaults - cstore := cryptostore.New( - cryptostore.SecretBox, - memstorage.New(), - keys.MustLoadCodec("english"), - ) - - // build your http server - ks := server.New(cstore, "ed25519") - r := mux.NewRouter() - sk := r.PathPrefix("/keys").Subrouter() - ks.Register(sk) - return r -} - -// return data, status code, and error -func listKeys(h http.Handler) (keys.Infos, int, error) { - rr := httptest.NewRecorder() - req, err := http.NewRequest("GET", "/keys/", nil) - if err != nil { - return nil, 0, err - } - - h.ServeHTTP(rr, req) - if http.StatusOK != rr.Code { - return nil, rr.Code, nil - } - - data := keys.Infos{} - err = json.Unmarshal(rr.Body.Bytes(), &data) - return data, rr.Code, err -} - -func getKey(h http.Handler, name string) (*keys.Info, int, error) { - rr := httptest.NewRecorder() - req, err := http.NewRequest("GET", "/keys/"+name, nil) - if err != nil { - return nil, 0, err - } - - h.ServeHTTP(rr, req) - if http.StatusOK != rr.Code { - return nil, rr.Code, nil - } - - data := keys.Info{} - err = json.Unmarshal(rr.Body.Bytes(), &data) - return &data, rr.Code, err -} - -func createKey(h http.Handler, name, passphrase, algo string) (*types.CreateKeyResponse, int, error) { - rr := httptest.NewRecorder() - post := types.CreateKeyRequest{ - Name: name, - Passphrase: passphrase, - Algo: algo, - } - var b bytes.Buffer - err := json.NewEncoder(&b).Encode(&post) - if err != nil { - return nil, 0, err - } - - req, err := http.NewRequest("POST", "/keys/", &b) - if err != nil { - return nil, 0, err - } - - h.ServeHTTP(rr, req) - if http.StatusOK != rr.Code { - return nil, rr.Code, nil - } - - data := new(types.CreateKeyResponse) - err = json.Unmarshal(rr.Body.Bytes(), data) - return data, rr.Code, err -} - -func deleteKey(h http.Handler, name, passphrase string) (*types.ErrorResponse, int, error) { - rr := httptest.NewRecorder() - post := types.DeleteKeyRequest{ - Name: name, - Passphrase: passphrase, - } - var b bytes.Buffer - err := json.NewEncoder(&b).Encode(&post) - if err != nil { - return nil, 0, err - } - - req, err := http.NewRequest("DELETE", "/keys/"+name, &b) - if err != nil { - return nil, 0, err - } - - h.ServeHTTP(rr, req) - if http.StatusOK != rr.Code { - return nil, rr.Code, nil - } - - data := types.ErrorResponse{} - err = json.Unmarshal(rr.Body.Bytes(), &data) - return &data, rr.Code, err -} diff --git a/keys/server/types/keys.go b/keys/server/types/keys.go deleted file mode 100644 index 56ed60ceb..000000000 --- a/keys/server/types/keys.go +++ /dev/null @@ -1,35 +0,0 @@ -package types - -import "github.com/tendermint/go-crypto/keys" - -// CreateKeyRequest is sent to create a new key -type CreateKeyRequest struct { - Name string `json:"name" validate:"required,min=4,printascii"` - Passphrase string `json:"passphrase" validate:"required,min=10"` - Algo string `json:"algo"` -} - -// DeleteKeyRequest to destroy a key permanently (careful!) -type DeleteKeyRequest struct { - Name string `json:"name" validate:"required,min=4,printascii"` - Passphrase string `json:"passphrase" validate:"required,min=10"` -} - -// UpdateKeyRequest is sent to update the passphrase for an existing key -type UpdateKeyRequest struct { - Name string `json:"name" validate:"required,min=4,printascii"` - OldPass string `json:"passphrase" validate:"required,min=10"` - NewPass string `json:"new_passphrase" validate:"required,min=10"` -} - -// ErrorResponse is returned for 4xx and 5xx errors -type ErrorResponse struct { - Success bool `json:"success"` - Error string `json:"error"` // error message if Success is false - Code int `json:"code"` // error code if Success is false -} - -type CreateKeyResponse struct { - Key keys.Info `json:"key"` - Seed string `json:"seed_phrase"` -} diff --git a/keys/server/valid.go b/keys/server/valid.go deleted file mode 100644 index 50b51e21b..000000000 --- a/keys/server/valid.go +++ /dev/null @@ -1,12 +0,0 @@ -package server - -import ( - "github.com/pkg/errors" - "gopkg.in/go-playground/validator.v9" -) - -var v = validator.New() - -func validate(req interface{}) error { - return errors.Wrap(v.Struct(req), "Validate") -} diff --git a/keys/transactions.go b/keys/transactions.go index 10da7a6fa..1834ada26 100644 --- a/keys/transactions.go +++ b/keys/transactions.go @@ -1,9 +1,11 @@ package keys import ( + "fmt" "sort" crypto "github.com/tendermint/go-crypto" + wire "github.com/tendermint/go-wire" data "github.com/tendermint/go-wire/data" ) @@ -72,3 +74,52 @@ type Manager interface { 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 +} diff --git a/nano/keys.go b/nano/keys.go new file mode 100644 index 000000000..a6d3ea8e4 --- /dev/null +++ b/nano/keys.go @@ -0,0 +1,294 @@ +package nano + +import ( + "bytes" + "encoding/hex" + + "github.com/pkg/errors" + + ledger "github.com/ethanfrey/ledger" + + crypto "github.com/tendermint/go-crypto" + wire "github.com/tendermint/go-wire" +) + +//nolint +const ( + NameLedgerEd25519 = "ledger-ed25519" + TypeLedgerEd25519 = 0x10 + + // Timeout is the number of seconds to wait for a response from the ledger + // if eg. waiting for user confirmation on button push + Timeout = 20 +) + +var device *ledger.Ledger + +// getLedger gets a copy of the device, and caches it +func getLedger() (*ledger.Ledger, error) { + var err error + if device == nil { + device, err = ledger.FindLedger() + } + return device, err +} + +func signLedger(device *ledger.Ledger, msg []byte) (pk crypto.PubKey, sig crypto.Signature, err error) { + var resp []byte + + packets := generateSignRequests(msg) + for _, pack := range packets { + resp, err = device.Exchange(pack, Timeout) + if err != nil { + return pk, sig, err + } + } + + // the last call is the result we want and needs to be parsed + key, bsig, err := parseDigest(resp) + if err != nil { + return pk, sig, err + } + + var b [32]byte + copy(b[:], key) + return PubKeyLedgerEd25519FromBytes(b), crypto.SignatureEd25519FromBytes(bsig), nil +} + +// PrivKeyLedgerEd25519 implements PrivKey, calling the ledger nano +// we cache the PubKey from the first call to use it later +type PrivKeyLedgerEd25519 struct { + // PubKey should be private, but we want to encode it via go-wire + // so we can view the address later, even without having the ledger + // attached + CachedPubKey crypto.PubKey +} + +// NewPrivKeyLedgerEd25519Ed25519 will generate a new key and store the +// public key for later use. +func NewPrivKeyLedgerEd25519Ed25519() (crypto.PrivKey, error) { + var pk PrivKeyLedgerEd25519 + // getPubKey will cache the pubkey for later use, + // this allows us to return an error early if the ledger + // is not plugged in + _, err := pk.getPubKey() + return pk.Wrap(), err +} + +// ValidateKey allows us to verify the sanity of a key +// after loading it from disk +func (pk *PrivKeyLedgerEd25519) ValidateKey() error { + // getPubKey will return an error if the ledger is not + // properly set up... + pub, err := pk.forceGetPubKey() + if err != nil { + return err + } + // verify this matches cached address + if !pub.Equals(pk.CachedPubKey) { + return errors.New("ledger doesn't match cached key") + } + return nil +} + +// AssertIsPrivKeyInner fulfils PrivKey Interface +func (pk *PrivKeyLedgerEd25519) AssertIsPrivKeyInner() {} + +// Bytes fulfils pk Interface - stores the cached pubkey so we can verify +// the same key when we reconnect to a ledger +func (pk *PrivKeyLedgerEd25519) Bytes() []byte { + return wire.BinaryBytes(pk.Wrap()) +} + +// Sign calls the ledger and stores the pk for future use +// +// XXX/TODO: panics if there is an error communicating with the ledger. +// +// Communication is checked on NewPrivKeyLedger and PrivKeyFromBytes, +// returning an error, so this should only trigger if the privkey is held +// in memory for a while before use. +func (pk *PrivKeyLedgerEd25519) Sign(msg []byte) crypto.Signature { + // oh, I wish there was better error handling + dev, err := getLedger() + if err != nil { + panic(err) + } + + pub, sig, err := signLedger(dev, msg) + if err != nil { + panic(err) + } + + // if we have no pubkey yet, store it for future queries + if pk.CachedPubKey.Empty() { + pk.CachedPubKey = pub + } else if !pk.CachedPubKey.Equals(pub) { + panic("signed with a different key than stored") + } + return sig +} + +// PubKey returns the stored PubKey +// TODO: query the ledger if not there, once it is not volatile +func (pk *PrivKeyLedgerEd25519) PubKey() crypto.PubKey { + key, err := pk.getPubKey() + if err != nil { + panic(err) + } + return key +} + +// getPubKey reads the pubkey from cache or from the ledger itself +// since this involves IO, it may return an error, which is not exposed +// in the PubKey interface, so this function allows better error handling +func (pk *PrivKeyLedgerEd25519) getPubKey() (key crypto.PubKey, err error) { + // if we have no pubkey, set it + if pk.CachedPubKey.Empty() { + pk.CachedPubKey, err = pk.forceGetPubKey() + } + return pk.CachedPubKey, err +} + +// forceGetPubKey is like getPubKey but ignores any cached key +// and ensures we get it from the ledger itself. +func (pk *PrivKeyLedgerEd25519) forceGetPubKey() (key crypto.PubKey, err error) { + dev, err := getLedger() + if err != nil { + return key, errors.New("Can't connect to ledger device") + } + key, _, err = signLedger(dev, []byte{0}) + if err != nil { + return key, errors.New("Please open cosmos app on the ledger") + } + return key, err +} + +// Equals fulfils PrivKey Interface - makes sure both keys refer to the +// same +func (pk *PrivKeyLedgerEd25519) Equals(other crypto.PrivKey) bool { + if ledger, ok := other.Unwrap().(*PrivKeyLedgerEd25519); ok { + return pk.CachedPubKey.Equals(ledger.CachedPubKey) + } + return false +} + +// MockPrivKeyLedgerEd25519 behaves as the ledger, but stores a pre-packaged call-response +// for use in test cases +type MockPrivKeyLedgerEd25519 struct { + Msg []byte + Pub [KeyLength]byte + Sig [SigLength]byte +} + +// NewMockKey returns +func NewMockKey(msg, pubkey, sig string) (pk MockPrivKeyLedgerEd25519) { + var err error + pk.Msg, err = hex.DecodeString(msg) + if err != nil { + panic(err) + } + + bpk, err := hex.DecodeString(pubkey) + if err != nil { + panic(err) + } + bsig, err := hex.DecodeString(sig) + if err != nil { + panic(err) + } + + copy(pk.Pub[:], bpk) + copy(pk.Sig[:], bsig) + return pk +} + +var _ crypto.PrivKeyInner = MockPrivKeyLedgerEd25519{} + +// AssertIsPrivKeyInner fulfils PrivKey Interface +func (pk MockPrivKeyLedgerEd25519) AssertIsPrivKeyInner() {} + +// Bytes fulfils PrivKey Interface - not supported +func (pk MockPrivKeyLedgerEd25519) Bytes() []byte { + return nil +} + +// Sign returns a real SignatureLedger, if the msg matches what we expect +func (pk MockPrivKeyLedgerEd25519) Sign(msg []byte) crypto.Signature { + if !bytes.Equal(pk.Msg, msg) { + panic("Mock key is for different msg") + } + return crypto.SignatureEd25519(pk.Sig).Wrap() +} + +// PubKey returns a real PubKeyLedgerEd25519, that will verify this signature +func (pk MockPrivKeyLedgerEd25519) PubKey() crypto.PubKey { + return PubKeyLedgerEd25519FromBytes(pk.Pub) +} + +// Equals compares that two Mocks have the same data +func (pk MockPrivKeyLedgerEd25519) Equals(other crypto.PrivKey) bool { + if mock, ok := other.Unwrap().(MockPrivKeyLedgerEd25519); ok { + return bytes.Equal(mock.Pub[:], pk.Pub[:]) && + bytes.Equal(mock.Sig[:], pk.Sig[:]) && + bytes.Equal(mock.Msg, pk.Msg) + } + return false +} + +//////////////////////////////////////////// +// pubkey + +// PubKeyLedgerEd25519 works like a normal Ed25519 except a hash before the verify bytes +type PubKeyLedgerEd25519 struct { + crypto.PubKeyEd25519 +} + +// PubKeyLedgerEd25519FromBytes creates a PubKey from the raw bytes +func PubKeyLedgerEd25519FromBytes(key [32]byte) crypto.PubKey { + return PubKeyLedgerEd25519{crypto.PubKeyEd25519(key)}.Wrap() +} + +// Bytes fulfils pk Interface - no data, just type info +func (pk PubKeyLedgerEd25519) Bytes() []byte { + return wire.BinaryBytes(pk.Wrap()) +} + +// VerifyBytes uses the normal Ed25519 algorithm but a sha512 hash beforehand +func (pk PubKeyLedgerEd25519) VerifyBytes(msg []byte, sig crypto.Signature) bool { + hmsg := hashMsg(msg) + return pk.PubKeyEd25519.VerifyBytes(hmsg, sig) +} + +// Equals implements PubKey interface +func (pk PubKeyLedgerEd25519) Equals(other crypto.PubKey) bool { + if ledger, ok := other.Unwrap().(PubKeyLedgerEd25519); ok { + return pk.PubKeyEd25519.Equals(ledger.PubKeyEd25519.Wrap()) + } + return false +} + +/*** registration with go-data ***/ + +func init() { + crypto.PrivKeyMapper. + RegisterImplementation(&PrivKeyLedgerEd25519{}, NameLedgerEd25519, TypeLedgerEd25519). + RegisterImplementation(MockPrivKeyLedgerEd25519{}, "mock-ledger", 0x11) + + crypto.PubKeyMapper. + RegisterImplementation(PubKeyLedgerEd25519{}, NameLedgerEd25519, TypeLedgerEd25519) +} + +// Wrap fulfils interface for PrivKey struct +func (pk *PrivKeyLedgerEd25519) Wrap() crypto.PrivKey { + return crypto.PrivKey{PrivKeyInner: pk} +} + +// Wrap fulfils interface for PrivKey struct +func (pk MockPrivKeyLedgerEd25519) Wrap() crypto.PrivKey { + return crypto.PrivKey{PrivKeyInner: pk} +} + +// Wrap fulfils interface for PubKey struct +func (pk PubKeyLedgerEd25519) Wrap() crypto.PubKey { + return crypto.PubKey{PubKeyInner: pk} +} diff --git a/nano/keys_test.go b/nano/keys_test.go new file mode 100644 index 000000000..15aa0d545 --- /dev/null +++ b/nano/keys_test.go @@ -0,0 +1,142 @@ +package nano + +import ( + "encoding/hex" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + crypto "github.com/tendermint/go-crypto" +) + +func TestLedgerKeys(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + cases := []struct { + msg, pubkey, sig string + valid bool + }{ + 0: { + msg: "F00D", + pubkey: "8E8754F012C2FDB492183D41437FD837CB81D8BBE731924E2E0DAF43FD3F2C93", + sig: "787DC03E9E4EE05983E30BAE0DEFB8DB0671DBC2F5874AC93F8D8CA4018F7A42D6F9A9BCEADB422AC8E27CEE9CA205A0B88D22CD686F0A43EB806E8190A3C400", + valid: true, + }, + 1: { + msg: "DEADBEEF", + pubkey: "0C45ADC887A5463F668533443C829ED13EA8E2E890C778957DC28DB9D2AD5A6C", + sig: "00ED74EED8FDAC7988A14BF6BC222120CBAC249D569AF4C2ADABFC86B792F97DF73C4919BE4B6B0ACB53547273BF29FBF0A9E0992FFAB6CB6C9B09311FC86A00", + valid: true, + }, + 2: { + msg: "1234567890AA", + pubkey: "598FC1F0C76363D14D7480736DEEF390D85863360F075792A6975EFA149FD7EA", + sig: "59AAB7D7BDC4F936B6415DE672A8B77FA6B8B3451CD95B3A631F31F9A05DAEEE5E7E4F89B64DDEBB5F63DC042CA13B8FCB8185F82AD7FD5636FFDA6B0DC9570B", + valid: true, + }, + 3: { + msg: "1234432112344321", + pubkey: "359E0636E780457294CCA5D2D84DB190C3EDBD6879729C10D3963DEA1D5D8120", + sig: "616B44EC7A65E7C719C170D669A47DE80C6AC0BB13FBCC89230976F9CC14D4CF9ECF26D4AFBB9FFF625599F1FF6F78EDA15E9F6B6BDCE07CFE9D8C407AC45208", + valid: true, + }, + 4: { + msg: "12344321123443", + pubkey: "359E0636E780457294CCA5D2D84DB190C3EDBD6879729C10D3963DEA1D5D8120", + sig: "616B44EC7A65E7C719C170D669A47DE80C6AC0BB13FBCC89230976F9CC14D4CF9ECF26D4AFBB9FFF625599F1FF6F78EDA15E9F6B6BDCE07CFE9D8C407AC45208", + valid: false, + }, + 5: { + msg: "1234432112344321", + pubkey: "459E0636E780457294CCA5D2D84DB190C3EDBD6879729C10D3963DEA1D5D8120", + sig: "616B44EC7A65E7C719C170D669A47DE80C6AC0BB13FBCC89230976F9CC14D4CF9ECF26D4AFBB9FFF625599F1FF6F78EDA15E9F6B6BDCE07CFE9D8C407AC45208", + valid: false, + }, + 6: { + msg: "1234432112344321", + pubkey: "359E0636E780457294CCA5D2D84DB190C3EDBD6879729C10D3963DEA1D5D8120", + sig: "716B44EC7A65E7C719C170D669A47DE80C6AC0BB13FBCC89230976F9CC14D4CF9ECF26D4AFBB9FFF625599F1FF6F78EDA15E9F6B6BDCE07CFE9D8C407AC45208", + valid: false, + }, + } + + for i, tc := range cases { + bmsg, err := hex.DecodeString(tc.msg) + require.NoError(err, "%d", i) + + priv := NewMockKey(tc.msg, tc.pubkey, tc.sig) + pub := priv.PubKey() + sig := priv.Sign(bmsg) + + valid := pub.VerifyBytes(bmsg, sig) + assert.Equal(tc.valid, valid, "%d", i) + } +} + +func TestRealLedger(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + if os.Getenv("WITH_LEDGER") == "" { + t.Skip("Set WITH_LEDGER to run code on real ledger") + } + msg := []byte("kuhehfeohg") + + priv, err := NewPrivKeyLedgerEd25519Ed25519() + require.Nil(err, "%+v", err) + pub := priv.PubKey() + sig := priv.Sign(msg) + + valid := pub.VerifyBytes(msg, sig) + assert.True(valid) + + // now, let's serialize the key and make sure it still works + bs := priv.Bytes() + priv2, err := crypto.PrivKeyFromBytes(bs) + require.Nil(err, "%+v", err) + + // make sure we get the same pubkey when we load from disk + pub2 := priv2.PubKey() + require.Equal(pub, pub2) + + // signing with the loaded key should match the original pubkey + sig = priv2.Sign(msg) + valid = pub.VerifyBytes(msg, sig) + assert.True(valid) + + // make sure pubkeys serialize properly as well + bs = pub.Bytes() + bpub, err := crypto.PubKeyFromBytes(bs) + require.NoError(err) + assert.Equal(pub, bpub) +} + +// TestRealLedgerErrorHandling calls. These tests assume +// the ledger is not plugged in.... +func TestRealLedgerErrorHandling(t *testing.T) { + require := require.New(t) + + if os.Getenv("WITH_LEDGER") != "" { + t.Skip("Skipping on WITH_LEDGER as it tests unplugged cases") + } + + // first, try to generate a key, must return an error + // (no panic) + _, err := NewPrivKeyLedgerEd25519Ed25519() + require.Error(err) + + led := PrivKeyLedgerEd25519{} // empty + // or with some pub key + ed := crypto.GenPrivKeyEd25519() + led2 := PrivKeyLedgerEd25519{CachedPubKey: ed.PubKey()} + + // loading these should return errors + bs := led.Bytes() + _, err = crypto.PrivKeyFromBytes(bs) + require.Error(err) + + bs = led2.Bytes() + _, err = crypto.PrivKeyFromBytes(bs) + require.Error(err) +} diff --git a/nano/sign.go b/nano/sign.go new file mode 100644 index 000000000..c40801583 --- /dev/null +++ b/nano/sign.go @@ -0,0 +1,63 @@ +package nano + +import ( + "bytes" + "crypto/sha512" + + "github.com/pkg/errors" +) + +const ( + App = 0x80 + Init = 0x00 + Update = 0x01 + Digest = 0x02 + MaxChunk = 253 + KeyLength = 32 + SigLength = 64 +) + +var separator = []byte{0, 0xCA, 0xFE, 0} + +func generateSignRequests(payload []byte) [][]byte { + // nice one-shot + digest := []byte{App, Digest} + if len(payload) < MaxChunk { + return [][]byte{append(digest, payload...)} + } + + // large payload is multi-chunk + result := [][]byte{{App, Init}} + update := []byte{App, Update} + for len(payload) > MaxChunk { + msg := append(update, payload[:MaxChunk]...) + payload = payload[MaxChunk:] + result = append(result, msg) + } + result = append(result, append(update, payload...)) + result = append(result, digest) + return result +} + +func parseDigest(resp []byte) (key, sig []byte, err error) { + if resp[0] != App || resp[1] != Digest { + return nil, nil, errors.New("Invalid header") + } + resp = resp[2:] + if len(resp) != KeyLength+SigLength+len(separator) { + return nil, nil, errors.Errorf("Incorrect length: %d", len(resp)) + } + + key, resp = resp[:KeyLength], resp[KeyLength:] + if !bytes.Equal(separator, resp[:len(separator)]) { + return nil, nil, errors.New("Cannot find 0xCAFE") + } + + sig = resp[len(separator):] + return key, sig, nil +} + +func hashMsg(data []byte) []byte { + res := sha512.Sum512(data) + return res[:] +} diff --git a/nano/sign_test.go b/nano/sign_test.go new file mode 100644 index 000000000..04a6d0be7 --- /dev/null +++ b/nano/sign_test.go @@ -0,0 +1,159 @@ +package nano + +import ( + "encoding/hex" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + crypto "github.com/tendermint/go-crypto" +) + +func parseEdKey(data []byte) (key crypto.PubKey, err error) { + ed := crypto.PubKeyEd25519{} + if len(data) < len(ed) { + return key, errors.Errorf("Key length too short: %d", len(data)) + } + copy(ed[:], data) + return ed.Wrap(), nil +} + +func parseSig(data []byte) (key crypto.Signature, err error) { + ed := crypto.SignatureEd25519{} + if len(data) < len(ed) { + return key, errors.Errorf("Sig length too short: %d", len(data)) + } + copy(ed[:], data) + return ed.Wrap(), nil +} + +func TestParseDigest(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + cases := []struct { + output string + key string + sig string + valid bool + }{ + { + output: "80028E8754F012C2FDB492183D41437FD837CB81D8BBE731924E2E0DAF43FD3F2C9300CAFE00787DC03E9E4EE05983E30BAE0DEFB8DB0671DBC2F5874AC93F8D8CA4018F7A42D6F9A9BCEADB422AC8E27CEE9CA205A0B88D22CD686F0A43EB806E8190A3C400", + key: "8E8754F012C2FDB492183D41437FD837CB81D8BBE731924E2E0DAF43FD3F2C93", + sig: "787DC03E9E4EE05983E30BAE0DEFB8DB0671DBC2F5874AC93F8D8CA4018F7A42D6F9A9BCEADB422AC8E27CEE9CA205A0B88D22CD686F0A43EB806E8190A3C400", + valid: true, + }, + { + output: "800235467890876543525437890796574535467890", + key: "", + sig: "", + valid: false, + }, + } + + for i, tc := range cases { + msg, err := hex.DecodeString(tc.output) + require.Nil(err, "%d: %+v", i, err) + + lKey, lSig, err := parseDigest(msg) + if !tc.valid { + assert.NotNil(err, "%d", i) + } else if assert.Nil(err, "%d: %+v", i, err) { + key, err := hex.DecodeString(tc.key) + require.Nil(err, "%d: %+v", i, err) + sig, err := hex.DecodeString(tc.sig) + require.Nil(err, "%d: %+v", i, err) + + assert.Equal(key, lKey, "%d", i) + assert.Equal(sig, lSig, "%d", i) + } + } +} + +type cryptoCase struct { + msg string + key string + sig string + valid bool +} + +func toBytes(c cryptoCase) (msg, key, sig []byte, err error) { + msg, err = hex.DecodeString(c.msg) + if err != nil { + return + } + key, err = hex.DecodeString(c.key) + if err != nil { + return + } + sig, err = hex.DecodeString(c.sig) + return +} + +func TestCryptoConvert(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + cases := []cryptoCase{ + 0: { + msg: "F00D", + key: "8E8754F012C2FDB492183D41437FD837CB81D8BBE731924E2E0DAF43FD3F2C93", + sig: "787DC03E9E4EE05983E30BAE0DEFB8DB0671DBC2F5874AC93F8D8CA4018F7A42D6F9A9BCEADB422AC8E27CEE9CA205A0B88D22CD686F0A43EB806E8190A3C400", + valid: true, + }, + 1: { + msg: "DEADBEEF", + key: "0C45ADC887A5463F668533443C829ED13EA8E2E890C778957DC28DB9D2AD5A6C", + sig: "00ED74EED8FDAC7988A14BF6BC222120CBAC249D569AF4C2ADABFC86B792F97DF73C4919BE4B6B0ACB53547273BF29FBF0A9E0992FFAB6CB6C9B09311FC86A00", + valid: true, + }, + 2: { + msg: "1234567890AA", + key: "598FC1F0C76363D14D7480736DEEF390D85863360F075792A6975EFA149FD7EA", + sig: "59AAB7D7BDC4F936B6415DE672A8B77FA6B8B3451CD95B3A631F31F9A05DAEEE5E7E4F89B64DDEBB5F63DC042CA13B8FCB8185F82AD7FD5636FFDA6B0DC9570B", + valid: true, + }, + 3: { + msg: "1234432112344321", + key: "359E0636E780457294CCA5D2D84DB190C3EDBD6879729C10D3963DEA1D5D8120", + sig: "616B44EC7A65E7C719C170D669A47DE80C6AC0BB13FBCC89230976F9CC14D4CF9ECF26D4AFBB9FFF625599F1FF6F78EDA15E9F6B6BDCE07CFE9D8C407AC45208", + valid: true, + }, + 4: { + msg: "12344321123443", + key: "359E0636E780457294CCA5D2D84DB190C3EDBD6879729C10D3963DEA1D5D8120", + sig: "616B44EC7A65E7C719C170D669A47DE80C6AC0BB13FBCC89230976F9CC14D4CF9ECF26D4AFBB9FFF625599F1FF6F78EDA15E9F6B6BDCE07CFE9D8C407AC45208", + valid: false, + }, + 5: { + msg: "1234432112344321", + key: "459E0636E780457294CCA5D2D84DB190C3EDBD6879729C10D3963DEA1D5D8120", + sig: "616B44EC7A65E7C719C170D669A47DE80C6AC0BB13FBCC89230976F9CC14D4CF9ECF26D4AFBB9FFF625599F1FF6F78EDA15E9F6B6BDCE07CFE9D8C407AC45208", + valid: false, + }, + 6: { + msg: "1234432112344321", + key: "359E0636E780457294CCA5D2D84DB190C3EDBD6879729C10D3963DEA1D5D8120", + sig: "716B44EC7A65E7C719C170D669A47DE80C6AC0BB13FBCC89230976F9CC14D4CF9ECF26D4AFBB9FFF625599F1FF6F78EDA15E9F6B6BDCE07CFE9D8C407AC45208", + valid: false, + }, + } + + for i, tc := range cases { + msg, key, sig, err := toBytes(tc) + require.Nil(err, "%d: %+v", i, err) + + pk, err := parseEdKey(key) + require.Nil(err, "%d: %+v", i, err) + psig, err := parseSig(sig) + require.Nil(err, "%d: %+v", i, err) + + // it is not the signature of the message itself + valid := pk.VerifyBytes(msg, psig) + assert.False(valid, "%d", i) + + // but rather of the hash of the msg + hmsg := hashMsg(msg) + valid = pk.VerifyBytes(hmsg, psig) + assert.Equal(tc.valid, valid, "%d", i) + } +} diff --git a/priv_key.go b/priv_key.go index 0c6bd2ae7..e6e7ac036 100644 --- a/priv_key.go +++ b/priv_key.go @@ -13,13 +13,27 @@ import ( func PrivKeyFromBytes(privKeyBytes []byte) (privKey PrivKey, err error) { err = wire.ReadBinaryBytes(privKeyBytes, &privKey) + if err == nil { + // add support for a ValidateKey method on PrivKeys + // to make sure they load correctly + val, ok := privKey.Unwrap().(validatable) + if ok { + err = val.ValidateKey() + } + } return } +// validatable is an optional interface for keys that want to +// check integrity +type validatable interface { + ValidateKey() error +} + //---------------------------------------- // DO NOT USE THIS INTERFACE. -// You probably want to use PubKey +// You probably want to use PrivKey // +gen wrapper:"PrivKey,Impl[PrivKeyEd25519,PrivKeySecp256k1],ed25519,secp256k1" type PrivKeyInner interface { AssertIsPrivKeyInner() diff --git a/priv_key_test.go b/priv_key_test.go new file mode 100644 index 000000000..154df5593 --- /dev/null +++ b/priv_key_test.go @@ -0,0 +1,65 @@ +package crypto + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + wire "github.com/tendermint/go-wire" +) + +type BadKey struct { + PrivKeyEd25519 +} + +// Wrap fulfils interface for PrivKey struct +func (pk BadKey) Wrap() PrivKey { + return PrivKey{pk} +} + +func (pk BadKey) Bytes() []byte { + return wire.BinaryBytes(pk.Wrap()) +} + +func (pk BadKey) ValidateKey() error { + return fmt.Errorf("fuggly key") +} + +func init() { + PrivKeyMapper. + RegisterImplementation(BadKey{}, "bad", 0x66) +} + +func TestReadPrivKey(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + // garbage in, garbage out + garbage := []byte("hjgewugfbiewgofwgewr") + _, err := PrivKeyFromBytes(garbage) + require.Error(err) + + edKey := GenPrivKeyEd25519() + badKey := BadKey{edKey} + + cases := []struct { + key PrivKey + valid bool + }{ + {edKey.Wrap(), true}, + {badKey.Wrap(), false}, + } + + for i, tc := range cases { + data := tc.key.Bytes() + key, err := PrivKeyFromBytes(data) + if tc.valid { + assert.NoError(err, "%d", i) + assert.Equal(tc.key, key, "%d", i) + } else { + assert.Error(err, "%d: %#v", i, key) + } + } + +} diff --git a/signature.go b/signature.go index 5b1d6cb05..d2ea45132 100644 --- a/signature.go +++ b/signature.go @@ -63,6 +63,12 @@ func (sig *SignatureEd25519) UnmarshalJSON(enc []byte) error { return err } +func SignatureEd25519FromBytes(data []byte) Signature { + var sig SignatureEd25519 + copy(sig[:], data) + return sig.Wrap() +} + //------------------------------------- var _ SignatureInner = SignatureSecp256k1{}