diff --git a/CHANGELOG.md b/CHANGELOG.md index 63d4af1d8..64dc9998b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.4.0 (October 27, 2017) + +BREAKING CHANGES: + +- `keys`: use bcrypt plus salt + +FEATURES: + +- add support for signing via Ledger Nano + +IMPROVEMENTS: + +- linting and comments + ## 0.3.0 (September 22, 2017) BREAKING CHANGES: diff --git a/Makefile b/Makefile index 0f414e1c2..c1974c410 100644 --- a/Makefile +++ b/Makefile @@ -2,13 +2,15 @@ GOTOOLS = \ github.com/Masterminds/glide \ - github.com/jteeuwen/go-bindata/go-bindata + github.com/jteeuwen/go-bindata/go-bindata \ + github.com/alecthomas/gometalinter + REPO:=github.com/tendermint/go-crypto -all: get_vendor_deps test +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/ @@ -31,3 +33,37 @@ codegen: @echo "--> regenerating all interface wrappers" @gen @echo "Done!" + +metalinter: ensure_tools + @gometalinter --install + gometalinter --vendor --deadline=600s --enable-all --disable=lll ./... + +metalinter_test: ensure_tools + @gometalinter --install + gometalinter --vendor --deadline=600s --disable-all \ + --enable=deadcode \ + --enable=gas \ + --enable=goconst \ + --enable=gocyclo \ + --enable=gosimple \ + --enable=ineffassign \ + --enable=interfacer \ + --enable=maligned \ + --enable=megacheck \ + --enable=misspell \ + --enable=safesql \ + --enable=staticcheck \ + --enable=structcheck \ + --enable=unconvert \ + --enable=unused \ + --enable=vetshadow \ + --enable=vet \ + --enable=varcheck \ + ./... + + #--enable=dupl \ + #--enable=errcheck \ + #--enable=goimports \ + #--enable=golint \ <== comments on anything exported + #--enable=gotype \ + #--enable=unparam \ diff --git a/_gen.go b/_gen.go deleted file mode 100644 index a98feaf4e..000000000 --- a/_gen.go +++ /dev/null @@ -1,6 +0,0 @@ -package main - -import ( - _ "github.com/tendermint/go-wire/gen" - _ "github.com/clipperhouse/stringer" -) diff --git a/armor.go b/armor.go index 3d2eff5e7..5f199df43 100644 --- a/armor.go +++ b/armor.go @@ -22,7 +22,7 @@ func EncodeArmor(blockType string, headers map[string]string, data []byte) strin if err != nil { PanicSanity("Error encoding ascii armor: " + err.Error()) } - return string(buf.Bytes()) + return buf.String() } func DecodeArmor(armorStr string) (blockType string, headers map[string]string, data []byte, err error) { diff --git a/bcrypt/bcrypt.go b/bcrypt/bcrypt.go index a6b4a2cf6..6b23b7a9d 100644 --- a/bcrypt/bcrypt.go +++ b/bcrypt/bcrypt.go @@ -50,7 +50,7 @@ func (ih InvalidHashPrefixError) Error() string { type InvalidCostError int func (ic InvalidCostError) Error() string { - return fmt.Sprintf("crypto/bcrypt: cost %d is outside allowed range (%d,%d)", int(ic), int(MinCost), int(MaxCost)) + return fmt.Sprintf("crypto/bcrypt: cost %d is outside allowed range (%d,%d)", int(ic), int(MinCost), int(MaxCost)) // nolint: unconvert } const ( diff --git a/doc.go b/doc.go new file mode 100644 index 000000000..c6701bc58 --- /dev/null +++ b/doc.go @@ -0,0 +1,48 @@ +/* +go-crypto is a customized/convenience cryptography package +for supporting Tendermint. + +It wraps select functionality of equivalent functions in the +Go standard library, for easy usage with our libraries. + +Keys: + +All key generation functions return an instance of the PrivKey interface +which implements methods + + AssertIsPrivKeyInner() + Bytes() []byte + Sign(msg []byte) Signature + PubKey() PubKey + Equals(PrivKey) bool + Wrap() PrivKey + +From the above method we can: +a) Retrieve the public key if needed + + pubKey := key.PubKey() + +For example: + privKey, err := crypto.GenPrivKeyEd25519() + if err != nil { + ... + } + pubKey := privKey.PubKey() + ... + // And then you can use the private and public key + doSomething(privKey, pubKey) + + +We also provide hashing wrappers around algorithms: + +Sha256 + sum := crypto.Sha256([]byte("This is Tendermint")) + fmt.Printf("%x\n", sum) + +Ripemd160 + sum := crypto.Ripemd160([]byte("This is consensus")) + fmt.Printf("%x\n", sum) +*/ +package crypto + +// TODO: Add more docs in here diff --git a/embed_test.go b/embed_test.go index e2d2fe504..e5c37c0c1 100644 --- a/embed_test.go +++ b/embed_test.go @@ -73,8 +73,8 @@ func TestEncodeDemo(t *testing.T) { // Try to encode as binary b, err := data.ToWire(tc.in) if assert.Nil(err, "%d: %#v", i, tc.in) { - err := data.FromWire(b, tc.out) - if assert.Nil(err) { + err2 := data.FromWire(b, tc.out) + if assert.Nil(err2) { assert.Equal(tc.expected, tc.out.String()) } } @@ -82,8 +82,8 @@ func TestEncodeDemo(t *testing.T) { // Try to encode it as json j, err := data.ToJSON(tc.in) if assert.Nil(err, "%d: %#v", i, tc.in) { - err := data.FromJSON(j, tc.out) - if assert.Nil(err) { + err2 := data.FromJSON(j, tc.out) + if assert.Nil(err2) { assert.Equal(tc.expected, tc.out.String()) } } diff --git a/example_test.go b/example_test.go new file mode 100644 index 000000000..2c8b94549 --- /dev/null +++ b/example_test.go @@ -0,0 +1,35 @@ +// Copyright 2017 Tendermint. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crypto_test + +import ( + "fmt" + + "github.com/tendermint/go-crypto" +) + +func ExampleSha256() { + sum := crypto.Sha256([]byte("This is Tendermint")) + fmt.Printf("%x\n", sum) + // Output: + // f91afb642f3d1c87c17eb01aae5cb65c242dfdbe7cf1066cc260f4ce5d33b94e +} + +func ExampleRipemd160() { + sum := crypto.Ripemd160([]byte("This is Tendermint")) + fmt.Printf("%x\n", sum) + // Output: + // 051e22663e8f0fd2f2302f1210f954adff009005 +} diff --git a/glide.lock b/glide.lock index 1c23d8bea..096ec5c15 100644 --- a/glide.lock +++ b/glide.lock @@ -1,28 +1,20 @@ -hash: c0a2db1b80c6b1b8aab31c526ce43e22e49b23c893c78b8fdb8546aa2e7b7cc6 -updated: 2017-09-22T10:21:34.220901552-04:00 +hash: 6e06a42eafe0aeff112cee86aef6b2cab0e2f62c2e6bfccfb629aa22f6b62773 +updated: 2017-10-27T18:45:18.350198941+02:00 imports: -- name: github.com/bgentry/speakeasy - version: 4aabc24848ce5fd31929f7d1e4ea74d3709c14cd - name: github.com/btcsuite/btcd - version: b8df516b4b267acf2de46be593a9d948d1d2c420 + version: c7588cbf7690cd9f047a28efa2dcd8f2435a4e5e subpackages: - btcec - - chaincfg - - 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/hid + version: 660bb717bd4e7cbcdf0f7cd5cadf1cb2e4be452a +- name: github.com/ethanfrey/ledger + version: 23a7bb9d74bc83a862fcb4bddde24215b2295ad9 - name: github.com/go-kit/kit - version: d67bb4c202e3b91377d1079b110a6c9ce23ab2f8 + version: e2b298466b32c7cd5579a9b9b07e968fc9d9452c subpackages: - log - log/level @@ -30,46 +22,36 @@ imports: - name: github.com/go-logfmt/logfmt version: 390ab7935ee28ec6b286364bba9b4dd6410cb3d5 - name: github.com/go-playground/locales - version: 1e5f1161c6416a5ff48840eb8724a394e48cc534 + version: e4cbcb5d0652150d40ad0646651076b6bd2be4f6 subpackages: - currency - name: github.com/go-playground/universal-translator version: 71201497bace774495daed26a3874fd339e0b538 - name: github.com/go-stack/stack - version: 100eb0c0a9c5b306ca2fb4f165df21d80ada4b82 -- name: github.com/gorilla/context - version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 -- name: github.com/gorilla/handlers - version: a4043c62cc2329bacda331d33fc908ab11ef0ec3 -- name: github.com/gorilla/mux - version: bcd8bc72b08df0f70df986b97f95590779502d31 + version: 817915b46b97fd7bb80e8ab6b69f01a53ac3eebf - name: github.com/howeyc/crc16 version: 96a97a1abb579c7ff1a8ffa77f2e72d1c314b57f - name: github.com/kr/logfmt version: b84e30acd515aadc4b783ad4ff83aff3299bdfe0 - name: github.com/pkg/errors version: 645ef00459ed84a119197bfb8d8205042c6df63d -- name: github.com/spf13/cobra - version: 4cdb38c072b86bf795d2c81de50784d9fdd6eb77 -- name: github.com/spf13/viper - version: 0967fc9aceab2ce9da34061253ac10fb99bba5b2 - name: github.com/tendermint/ed25519 - version: 1f52c6f8b8a5c7908aff4497c186af344b428925 + version: d8387025d2b9d158cf4efb07e7ebf814bcce2057 subpackages: - edwards25519 - extra25519 - name: github.com/tendermint/go-wire - version: 5f88da3dbc1a72844e6dfaf274ce87f851d488eb + version: 8ee84b5b2581530168daf66fc89c548d27403c57 subpackages: - data - data/base58 - name: github.com/tendermint/tmlibs - version: bffe6744ec277d60f707ab442e25513617842f8e + version: 092eb701c7276907cdbed258750e22ce895b6735 subpackages: - common - log - name: golang.org/x/crypto - version: c7af5bf2638a1164f2eb5467c39c6cffbd13a02e + version: edd5e9b0879d13ee6970a50153d85b8fec9f7686 subpackages: - bcrypt - blowfish @@ -81,16 +63,12 @@ imports: - ripemd160 - salsa20/salsa - name: gopkg.in/go-playground/validator.v9 - version: d529ee1b0f30352444f507cc6cdac96bfd12decc + version: 1304298bf10d085adec514b076772a79c9cadb6b testImports: - 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 +81,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..2f3e72471 100644 --- a/glide.yaml +++ b/glide.yaml @@ -22,14 +22,9 @@ import: - nacl/secretbox - openpgp/armor - ripemd160 -- package: github.com/bgentry/speakeasy -- package: github.com/gorilla/handlers -- package: github.com/gorilla/mux - package: github.com/pkg/errors -- package: github.com/spf13/cobra -- 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/hd/address.go b/hd/address.go index d7553a4d7..5b664b496 100644 --- a/hd/address.go +++ b/hd/address.go @@ -11,7 +11,6 @@ import ( "errors" "fmt" "hash" - "log" "math/big" "strconv" "strings" @@ -86,8 +85,7 @@ func ComputeTxId(rawTxHex string) string { return HexEncode(ReverseBytes(CalcHash256(HexDecode(rawTxHex)))) } -// Private methods... - +/* func printKeyInfo(privKeyBytes []byte, pubKeyBytes []byte, chain []byte) { if pubKeyBytes == nil { pubKeyBytes = PubKeyBytesFromPrivKeyBytes(privKeyBytes, true) @@ -99,6 +97,7 @@ func printKeyInfo(privKeyBytes []byte, pubKeyBytes []byte, chain []byte) { addr, HexEncode(chain)) } +*/ func DerivePrivateKeyForPath(privKeyBytes []byte, chain []byte, path string) []byte { data := privKeyBytes @@ -144,7 +143,7 @@ func DerivePublicKeyForPath(pubKeyBytes []byte, chain []byte, path string) []byt } func DerivePrivateKey(privKeyBytes []byte, chain []byte, i uint32, prime bool) ([]byte, []byte) { - data := []byte{} + var data []byte if prime { i = i | 0x80000000 data = append([]byte{byte(0)}, privKeyBytes...) @@ -177,11 +176,11 @@ func addPoints(a []byte, b []byte) []byte { panic(err) } sumX, sumY := btcec.S256().Add(ap.X, ap.Y, bp.X, bp.Y) - sum := (*btcec.PublicKey)(&btcec.PublicKey{ + sum := &btcec.PublicKey{ Curve: btcec.S256(), X: sumX, Y: sumY, - }) + } return sum.SerializeCompressed() } @@ -248,11 +247,11 @@ func WIFFromPrivKeyBytes(privKeyBytes []byte, compress bool) string { func PubKeyBytesFromPrivKeyBytes(privKeyBytes []byte, compress bool) (pubKeyBytes []byte) { x, y := btcec.S256().ScalarBaseMult(privKeyBytes) - pub := (*btcec.PublicKey)(&btcec.PublicKey{ + pub := &btcec.PublicKey{ Curve: btcec.S256(), X: x, Y: y, - }) + } if compress { return pub.SerializeCompressed() diff --git a/hd/hd_test.go b/hd/hd_test.go index b2f7d2e8f..02d80388c 100644 --- a/hd/hd_test.go +++ b/hd/hd_test.go @@ -2,9 +2,9 @@ package hd import ( "bytes" - "crypto/hmac" - "crypto/sha512" - "encoding/binary" + //"crypto/hmac" + //"crypto/sha512" + //"encoding/binary" "encoding/hex" "encoding/json" "fmt" @@ -15,10 +15,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/tyler-smith/go-bip39" - "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcutil/hdkeychain" - "github.com/mndrix/btcutil" - "github.com/tyler-smith/go-bip32" + //"github.com/btcsuite/btcd/chaincfg" + //"github.com/btcsuite/btcutil/hdkeychain" + //"github.com/mndrix/btcutil" + //"github.com/tyler-smith/go-bip32" "github.com/tendermint/go-crypto" ) @@ -33,7 +33,7 @@ type addrData struct { } // NOTE: atom fundraiser address -var hdPath string = "m/44'/118'/0'/0/0" +// var hdPath string = "m/44'/118'/0'/0/0" var hdToAddrTable []addrData func init() { @@ -109,12 +109,14 @@ func TestReverseBytes(t *testing.T) { } } +/* func ifExit(err error, n int) { if err != nil { fmt.Println(n, err) os.Exit(1) } } +*/ func gocrypto(seed []byte) ([]byte, []byte, []byte) { @@ -131,6 +133,7 @@ func gocrypto(seed []byte) ([]byte, []byte, []byte) { return HexDecode(priv), privBytes, pubBytes } +/* func btcsuite(seed []byte) ([]byte, []byte, []byte) { fmt.Println("HD") masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams) @@ -207,9 +210,9 @@ func tylerSmith(seed []byte) ([]byte, []byte, []byte) { pub := k.PublicKey().Key return masterKey.Key, priv, pub } +*/ // Benchmarks - var revBytesCases = [][]byte{ nil, []byte(""), @@ -238,5 +241,6 @@ func BenchmarkReverseBytes(b *testing.B) { // sink is necessary to ensure if the compiler tries // to smart, that it won't optimize away the benchmarks. if sink != nil { + _ = sink } } diff --git a/keys/cryptostore/enc_storage.go b/keys/cryptostore/enc_storage.go index daeb220b5..e0c7e59a3 100644 --- a/keys/cryptostore/enc_storage.go +++ b/keys/cryptostore/enc_storage.go @@ -12,21 +12,21 @@ type encryptedStorage struct { } func (es encryptedStorage) Put(name, pass string, key crypto.PrivKey) error { - secret, err := es.coder.Encrypt(key, pass) + saltBytes, encBytes, err := es.coder.Encrypt(key, pass) if err != nil { return err } ki := info(name, key) - return es.store.Put(name, secret, ki) + return es.store.Put(name, saltBytes, encBytes, ki) } func (es encryptedStorage) Get(name, pass string) (crypto.PrivKey, keys.Info, error) { - secret, info, err := es.store.Get(name) + saltBytes, encBytes, info, err := es.store.Get(name) if err != nil { return crypto.PrivKey{}, info, err } - key, err := es.coder.Decrypt(secret, pass) + key, err := es.coder.Decrypt(saltBytes, encBytes, pass) return key, info, err } diff --git a/keys/cryptostore/encoder.go b/keys/cryptostore/encoder.go index 31cbc2e54..99241f1b7 100644 --- a/keys/cryptostore/encoder.go +++ b/keys/cryptostore/encoder.go @@ -2,7 +2,23 @@ package cryptostore import ( "github.com/pkg/errors" + crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/go-crypto/bcrypt" +) + +const ( + // BcryptCost is as parameter to increase the resistance of the + // encoded keys to brute force password guessing + // + // Jae: 14 is good today (2016) + // + // Ethan: loading the key (at each signing) takes a second on my desktop, + // this is hard for laptops and deadly for mobile. You can raise it again, + // but for now, I will make this usable + // + // TODO: review value + BCryptCost = 12 ) var ( @@ -16,45 +32,55 @@ var ( // // 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)) + Encrypt(privKey crypto.PrivKey, passphrase string) (saltBytes []byte, encBytes []byte, err error) + Decrypt(saltBytes []byte, encBytes []byte, passphrase string) (privKey crypto.PrivKey, err error) } type secretbox struct{} -func (e secretbox) Encrypt(key crypto.PrivKey, pass string) ([]byte, error) { - if pass == "" { - return key.Bytes(), nil +func (e secretbox) Encrypt(privKey crypto.PrivKey, passphrase string) (saltBytes []byte, encBytes []byte, err error) { + if passphrase == "" { + return nil, privKey.Bytes(), nil } - s := secret(pass) - cipher := crypto.EncryptSymmetric(key.Bytes(), s) - return cipher, nil + + saltBytes = crypto.CRandBytes(16) + key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), BCryptCost) + if err != nil { + return nil, nil, errors.Wrap(err, "Couldn't generate bcrypt key from passphrase.") + } + key = crypto.Sha256(key) // Get 32 bytes + privKeyBytes := privKey.Bytes() + return saltBytes, crypto.EncryptSymmetric(privKeyBytes, key), 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) +func (e secretbox) Decrypt(saltBytes []byte, encBytes []byte, passphrase string) (privKey crypto.PrivKey, err error) { + privKeyBytes := encBytes + // NOTE: Some keys weren't encrypted with a passphrase and hence we have the conditional + if passphrase != "" { + var key []byte + key, err = bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), BCryptCost) if err != nil { return crypto.PrivKey{}, errors.Wrap(err, "Invalid Passphrase") } + key = crypto.Sha256(key) // Get 32 bytes + privKeyBytes, err = crypto.DecryptSymmetric(encBytes, key) + if err != nil { + return crypto.PrivKey{}, errors.Wrap(err, "Invalid Passphrase") + } + } + privKey, err = crypto.PrivKeyFromBytes(privKeyBytes) + if err != nil { + return crypto.PrivKey{}, errors.Wrap(err, "Private Key") } - key, err = crypto.PrivKeyFromBytes(private) - return key, errors.Wrap(err, "Invalid Passphrase") + return privKey, nil } type noop struct{} -func (n noop) Encrypt(key crypto.PrivKey, pass string) ([]byte, error) { - return key.Bytes(), nil +func (n noop) Encrypt(key crypto.PrivKey, passphrase string) (saltBytes []byte, encBytes []byte, err error) { + return []byte{}, key.Bytes(), nil } -func (n noop) Decrypt(data []byte, pass string) (crypto.PrivKey, error) { - return crypto.PrivKeyFromBytes(data) +func (n noop) Decrypt(saltBytes []byte, encBytes []byte, passphrase string) (privKey crypto.PrivKey, err error) { + return crypto.PrivKeyFromBytes(encBytes) } diff --git a/keys/cryptostore/encoder_test.go b/keys/cryptostore/encoder_test.go index f468591f3..614286a19 100644 --- a/keys/cryptostore/encoder_test.go +++ b/keys/cryptostore/encoder_test.go @@ -15,25 +15,27 @@ 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") + _, b, err := noop.Encrypt(key, "encode") require.Nil(err) assert.NotEmpty(b) - b2, err := noop.Encrypt(key2, "encode") + _, 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") + pk, err := noop.Decrypt(nil, b, "decode") require.Nil(err) require.NotNil(pk) assert.Equal(key, pk) - pk2, err := noop.Decrypt(b2, "kggugougp") + pk2, err := noop.Decrypt(nil, b2, "kggugougp") require.Nil(err) require.NotNil(pk2) assert.Equal(key2, pk2) @@ -43,20 +45,21 @@ 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" - b, err := enc.Encrypt(key, pass) + s, 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") + pk, err := enc.Decrypt(s, 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) + pk, err = enc.Decrypt(s, b, pass) require.Nil(err) assert.Equal(key, pk) } @@ -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 @@ -80,11 +84,11 @@ func TestSecretBoxNoPass(t *testing.T) { } for i, tc := range cases { - b, err := enc.Encrypt(key, tc.encode) + s, 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) + pk, err := enc.Decrypt(s, b, tc.decode) if tc.valid { require.Nil(err, "%d: %+v", i, err) assert.Equal(key, pk, "%d", i) @@ -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(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 0a2bb55c2..65cc8e58b 100644 --- a/keys/cryptostore/generator.go +++ b/keys/cryptostore/generator.go @@ -2,7 +2,9 @@ package cryptostore import ( "github.com/pkg/errors" + crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/go-crypto/nano" ) var ( @@ -10,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, 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 genSecp256(secret []byte) crypto.PrivKey { - return crypto.GenPrivKeySecp256k1FromSecret(secret).Wrap() +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, error) { +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 18437a9b5..923190c1b 100644 --- a/keys/cryptostore/holder.go +++ b/keys/cryptostore/holder.go @@ -24,29 +24,24 @@ func New(coder Encoder, store keys.Storage, codec keys.Codec) Manager { } } -// exists just to make sure we fulfill the Signer interface -func (s Manager) assertSigner() keys.Signer { - return s -} - -// exists just to make sure we fulfill the Manager interface -func (s Manager) assertKeyManager() keys.Manager { - return s -} +// 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) { - 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 @@ -80,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) @@ -100,7 +95,7 @@ func (s Manager) List() (keys.Infos, error) { // Get returns the public information about one key func (s Manager) Get(name string) (keys.Info, error) { - _, info, err := s.es.store.Get(name) + _, _, info, err := s.es.store.Get(name) return info, err } @@ -124,21 +119,23 @@ func (s Manager) Sign(name, passphrase string, tx keys.Signable) error { // // 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) { +// TODO: How to handle Export with salt? +func (s Manager) Export(name, oldpass, transferpass string) (salt, data []byte, err error) { key, _, err := s.es.Get(name, oldpass) if err != nil { - return nil, err + return nil, nil, err } - res, err := s.es.coder.Encrypt(key, transferpass) - return res, err + salt, data, err = s.es.coder.Encrypt(key, transferpass) + return salt, data, 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 +// If they are valid, it stores the key 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) +// TODO: How to handle Import with salt? +func (s Manager) Import(name, newpass, transferpass string, salt, data []byte) error { + key, err := s.es.coder.Decrypt(salt, data, transferpass) if err != nil { return err } diff --git a/keys/cryptostore/holder_test.go b/keys/cryptostore/holder_test.go index 434966f46..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 @@ -50,22 +54,22 @@ func TestKeyManagement(t *testing.T) { assert.NotNil(err) // list shows them in order - keys, err := cstore.List() + keyS, err := cstore.List() require.Nil(err) - require.Equal(2, len(keys)) + require.Equal(2, len(keyS)) // note these are in alphabetical order - assert.Equal(n2, keys[0].Name) - assert.Equal(n1, keys[1].Name) - assert.Equal(i2.PubKey, keys[0].PubKey) + assert.Equal(n2, keyS[0].Name) + assert.Equal(n1, keyS[1].Name) + assert.Equal(i2.PubKey, keyS[0].PubKey) // deleting a key removes it err = cstore.Delete("bad name", "foo") require.NotNil(err) err = cstore.Delete(n1, p1) require.Nil(err) - keys, err = cstore.List() + keyS, err = cstore.List() require.Nil(err) - assert.Equal(1, len(keys)) + assert.Equal(1, len(keyS)) _, err = cstore.Get(n1) assert.NotNil(err) @@ -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) - -// // make the storage with reasonable defaults -// cstore := cryptostore.New( -// cryptostore.GenSecp256k1, -// cryptostore.SecretBox, -// memstorage.New(), -// ) - -// 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) - -// err = cstore.Create(n2, p2) -// require.Nil(err) -// i2, err := cstore.Get(n2) -// require.Nil(err) - -// // 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) - -// // 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} - -// // 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) -// } -// } -// } -// } -// } +func TestSignVerify(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + // 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" + + // create two users and get their info + i1, _, err := cstore.Create(n1, p1, algo) + 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!") + + // 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 + 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}, + } + + 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, "", key.Bytes()) + err = cstore.Import(name, pass, "", nil, key.Bytes()) require.Nil(err, "%+v", err) // make sure the address matches @@ -209,15 +273,15 @@ func TestAdvancedKeyManagement(t *testing.T) { assertPassword(assert, cstore, n1, p2, p1) // exporting requires the proper name and passphrase - _, err = cstore.Export(n2, p2, pt) + _, _, err = cstore.Export(n2, p2, pt) assert.NotNil(err) - _, err = cstore.Export(n1, p1, pt) + _, _, err = cstore.Export(n1, p1, pt) assert.NotNil(err) - exported, err := cstore.Export(n1, p2, pt) + salt, exported, err := cstore.Export(n1, p2, pt) require.Nil(err, "%+v", err) // import fails on bad transfer pass - err = cstore.Import(n2, p3, p2, exported) + err = cstore.Import(n2, p3, p2, salt, exported) assert.NotNil(err) } @@ -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(), -// ) - -// // 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) -// } - -// // 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") -// } - -// // 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") -// } - -// // Output: -// // Alice -// // Bob -// // Carl -// // signed by Bob -// } +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 + 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 := 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") + if !binfo.PubKey.Equals(bob.PubKey) { + fmt.Println("Get and Create return different keys") + } + + 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 907a19f11..23931c294 100644 --- a/keys/cryptostore/storage_test.go +++ b/keys/cryptostore/storage_test.go @@ -5,16 +5,19 @@ import ( "github.com/stretchr/testify/assert" - crypto "github.com/tendermint/go-crypto" cmn "github.com/tendermint/tmlibs/common" + crypto "github.com/tendermint/go-crypto" keys "github.com/tendermint/go-crypto/keys" ) 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 710e4f392..000000000 --- a/keys/server/helpers.go +++ /dev/null @@ -1,58 +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" - - data "github.com/tendermint/go-wire/data" - "github.com/tendermint/go-crypto/keys/server/types" - - "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/storage.go b/keys/storage.go deleted file mode 100644 index 0c25eb8a5..000000000 --- a/keys/storage.go +++ /dev/null @@ -1,10 +0,0 @@ -package keys - -// 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) ([]byte, Info, error) - List() (Infos, error) - Delete(name string) error -} diff --git a/keys/storage/filestorage/main.go b/keys/storage/filestorage/main.go index 696b200fc..2bd4dcc1f 100644 --- a/keys/storage/filestorage/main.go +++ b/keys/storage/filestorage/main.go @@ -6,6 +6,7 @@ like standard ssh key storage. package filestorage import ( + "encoding/hex" "fmt" "io/ioutil" "os" @@ -13,19 +14,26 @@ import ( "strings" "github.com/pkg/errors" + crypto "github.com/tendermint/go-crypto" keys "github.com/tendermint/go-crypto/keys" ) const ( + // BlockType is the type of block. BlockType = "Tendermint Light Client" - PrivExt = "tlc" - PubExt = "pub" - keyPerm = os.FileMode(0600) - pubPerm = os.FileMode(0644) - dirPerm = os.FileMode(0700) + + // 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) ) +// FileStore is a file-based key storage with tight permissions. type FileStore struct { keyDir string } @@ -36,20 +44,20 @@ type FileStore struct { // 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} } -// assertStorage just makes sure we implement the proper Storage interface -func (s FileStore) assertStorage() keys.Storage { - return s -} +// 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 { +func (s FileStore) Put(name string, salt, key []byte, info keys.Info) error { pub, priv := s.nameToPaths(name) // write public info @@ -59,22 +67,22 @@ func (s FileStore) Put(name string, key []byte, info keys.Info) error { } // write private info - return write(priv, name, key) + return write(priv, name, salt, 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) { +func (s FileStore) Get(name string) (salt []byte, key []byte, info keys.Info, err error) { pub, priv := s.nameToPaths(name) - info, err := readInfo(pub) + info, err = readInfo(pub) if err != nil { - return nil, info, err + return nil, nil, info, err } - key, _, err := read(priv) - return key, info.Format(), err + salt, key, _, err = read(priv) + return salt, key, info.Format(), err } // List parses the key directory for public info and returns a list of @@ -84,6 +92,8 @@ func (s FileStore) List() (keys.Infos, error) { 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") @@ -111,61 +121,117 @@ func (s FileStore) List() (keys.Infos, error) { 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()) + return path.Join(s.keyDir, pubName), path.Join(s.keyDir, privName) } func readInfo(path string) (info keys.Info, err error) { - var data []byte - data, info.Name, err = read(path) + f, err := os.Open(path) if err != nil { - return + return info, errors.Wrap(err, "Reading data") } - pk, err := crypto.PubKeyFromBytes(data) + defer f.Close() + + d, err := ioutil.ReadAll(f) + if err != nil { + return info, errors.Wrap(err, "Reading data") + } + + block, headers, key, err := crypto.DecodeArmor(string(d)) + if err != nil { + return info, errors.Wrap(err, "Invalid Armor") + } + + if block != BlockType { + return info, errors.Errorf("Unknown key type: %s", block) + } + + pk, _ := crypto.PubKeyFromBytes(key) + info.Name = headers["name"] info.PubKey = pk - return + + return info, nil } -func read(path string) ([]byte, string, error) { +func read(path string) (salt, key []byte, name string, err error) { f, err := os.Open(path) if err != nil { - return nil, "", errors.Wrap(err, "Reading data") + return nil, nil, "", errors.Wrap(err, "Reading data") } + defer f.Close() + d, err := ioutil.ReadAll(f) if err != nil { - return nil, "", errors.Wrap(err, "Reading data") + return nil, nil, "", errors.Wrap(err, "Reading data") } + block, headers, key, err := crypto.DecodeArmor(string(d)) if err != nil { - return nil, "", errors.Wrap(err, "Invalid Armor") + return nil, nil, "", errors.Wrap(err, "Invalid Armor") } + if block != BlockType { - return nil, "", errors.Errorf("Unknown key type: %s", block) + return nil, nil, "", errors.Errorf("Unknown key type: %s", block) + } + + if headers["kdf"] != "bcrypt" { + return nil, nil, "", errors.Errorf("Unrecognized KDF type: %v", headers["kdf"]) + } + + if headers["salt"] == "" { + return nil, nil, "", errors.Errorf("Missing salt bytes") + } + + salt, err = hex.DecodeString(headers["salt"]) + if err != nil { + return nil, nil, "", errors.Errorf("Error decoding salt: %v", err.Error()) + } + + return salt, key, headers["name"], nil +} + +func writeInfo(path string, info keys.Info) 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") } - return key, headers["name"], nil + defer f.Close() + + headers := map[string]string{"name": info.Name} + text := crypto.EncodeArmor(BlockType, headers, info.PubKey.Bytes()) + _, err = f.WriteString(text) + + return errors.Wrap(err, "Writing data") } -func write(path, name string, key []byte) error { +func write(path, name string, salt, 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} + + headers := map[string]string{ + "name": name, + "kdf": "bcrypt", + "salt": fmt.Sprintf("%X", salt), + } + text := crypto.EncodeArmor(BlockType, headers, key) _, err = f.WriteString(text) + return errors.Wrap(err, "Writing data") } diff --git a/keys/storage/filestorage/main_test.go b/keys/storage/filestorage/main_test.go index 28c950c2c..bed46ab1f 100644 --- a/keys/storage/filestorage/main_test.go +++ b/keys/storage/filestorage/main_test.go @@ -22,6 +22,7 @@ func TestBasicCRUD(t *testing.T) { name := "bar" key := []byte("secret-key-here") + salt := []byte("salt-here") pubkey := crypto.GenPrivKeyEd25519().PubKey() info := keys.Info{ Name: name, @@ -29,7 +30,7 @@ func TestBasicCRUD(t *testing.T) { } // No data: Get and Delete return nothing - _, _, err = store.Get(name) + _, _, _, err = store.Get(name) assert.NotNil(err) err = store.Delete(name) assert.NotNil(err) @@ -39,14 +40,14 @@ func TestBasicCRUD(t *testing.T) { assert.Empty(l) // Putting the key in the store must work - err = store.Put(name, key, info) + err = store.Put(name, salt, key, info) assert.Nil(err) // But a second time is a failure - err = store.Put(name, key, info) + err = store.Put(name, salt, key, info) assert.NotNil(err) // Now, we can get and list properly - k, i, err := store.Get(name) + _, k, i, err := store.Get(name) require.Nil(err, "%+v", err) assert.Equal(key, k) assert.Equal(info.Name, i.Name) @@ -58,7 +59,7 @@ func TestBasicCRUD(t *testing.T) { assert.Equal(i, l[0]) // querying a non-existent key fails - _, _, err = store.Get("badname") + _, _, _, err = store.Get("badname") assert.NotNil(err) // We can only delete once @@ -68,7 +69,7 @@ func TestBasicCRUD(t *testing.T) { assert.NotNil(err) // and then Get and List don't work - _, _, err = store.Get(name) + _, _, _, err = store.Get(name) assert.NotNil(err) // List returns empty list l, err = store.List() diff --git a/keys/storage/memstorage/main.go b/keys/storage/memstorage/main.go index 195fa7a17..516aa40b8 100644 --- a/keys/storage/memstorage/main.go +++ b/keys/storage/memstorage/main.go @@ -7,11 +7,13 @@ package memstorage import ( "github.com/pkg/errors" + keys "github.com/tendermint/go-crypto/keys" ) type data struct { info keys.Info + salt []byte key []byte } @@ -22,29 +24,27 @@ func New() MemStore { return MemStore{} } -// assertStorage just makes sure we implement the Storage interface -func (s MemStore) assertStorage() keys.Storage { - return s -} +// 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 { +func (s MemStore) Put(name string, salt, 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} + s[name] = data{info, salt, 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 +func (s MemStore) Get(name string) (salt, key []byte, info keys.Info, 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 + + return d.salt, d.key, d.info.Format(), err } // List returns the public info of all keys in the MemStore in unsorted order diff --git a/keys/storage/memstorage/main_test.go b/keys/storage/memstorage/main_test.go index feccb387f..01975df58 100644 --- a/keys/storage/memstorage/main_test.go +++ b/keys/storage/memstorage/main_test.go @@ -14,6 +14,7 @@ func TestBasicCRUD(t *testing.T) { name := "foo" key := []byte("secret-key-here") + salt := []byte("salt-here") pubkey := crypto.GenPrivKeyEd25519().PubKey() info := keys.Info{ Name: name, @@ -21,7 +22,7 @@ func TestBasicCRUD(t *testing.T) { } // No data: Get and Delete return nothing - _, _, err := store.Get(name) + _, _, _, err := store.Get(name) assert.NotNil(err) err = store.Delete(name) assert.NotNil(err) @@ -31,14 +32,14 @@ func TestBasicCRUD(t *testing.T) { assert.Empty(l) // Putting the key in the store must work - err = store.Put(name, key, info) + err = store.Put(name, salt, key, info) assert.Nil(err) // But a second time is a failure - err = store.Put(name, key, info) + err = store.Put(name, salt, key, info) assert.NotNil(err) // Now, we can get and list properly - k, i, err := store.Get(name) + _, k, i, err := store.Get(name) assert.Nil(err) assert.Equal(key, k) assert.Equal(info.Name, i.Name) @@ -50,7 +51,7 @@ func TestBasicCRUD(t *testing.T) { assert.Equal(i, l[0]) // querying a non-existent key fails - _, _, err = store.Get("badname") + _, _, _, err = store.Get("badname") assert.NotNil(err) // We can only delete once @@ -60,7 +61,7 @@ func TestBasicCRUD(t *testing.T) { assert.NotNil(err) // and then Get and List don't work - _, _, err = store.Get(name) + _, _, _, err = store.Get(name) assert.NotNil(err) // List returns empty list l, err = store.List() diff --git a/keys/transactions.go b/keys/types.go similarity index 55% rename from keys/transactions.go rename to keys/types.go index 10da7a6fa..008a6f7ae 100644 --- a/keys/transactions.go +++ b/keys/types.go @@ -1,12 +1,23 @@ package keys import ( + "fmt" "sort" 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, salt []byte, key []byte, info Info) error + Get(name string) (salt []byte, 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"` @@ -72,3 +83,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/keys/wordcodec_test.go b/keys/wordcodec_test.go index 25c5439a6..a44607be2 100644 --- a/keys/wordcodec_test.go +++ b/keys/wordcodec_test.go @@ -119,8 +119,8 @@ func TestCheckInvalidLists(t *testing.T) { w, err := codec.BytesToWords(data) if tc.valid { assert.Nil(err, "%d: %+v", i, err) - b, err := codec.WordsToBytes(w) - assert.Nil(err, "%d: %+v", i, err) + b, err1 := codec.WordsToBytes(w) + assert.Nil(err1, "%d: %+v", i, err1) assert.Equal(data, b) } else { assert.NotNil(err, "%d", i) diff --git a/keys/wordlist/wordlist.go b/keys/wordlist/wordlist.go index 97ddb2369..58ff41181 100644 --- a/keys/wordlist/wordlist.go +++ b/keys/wordlist/wordlist.go @@ -204,9 +204,9 @@ func AssetNames() []string { // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "keys/wordlist/chinese_simplified.txt": keysWordlistChinese_simplifiedTxt, - "keys/wordlist/english.txt": keysWordlistEnglishTxt, - "keys/wordlist/japanese.txt": keysWordlistJapaneseTxt, - "keys/wordlist/spanish.txt": keysWordlistSpanishTxt, + "keys/wordlist/english.txt": keysWordlistEnglishTxt, + "keys/wordlist/japanese.txt": keysWordlistJapaneseTxt, + "keys/wordlist/spanish.txt": keysWordlistSpanishTxt, } // AssetDir returns the file names below a certain @@ -248,13 +248,14 @@ type bintree struct { Func func() (*asset, error) Children map[string]*bintree } + var _bintree = &bintree{nil, map[string]*bintree{ "keys": &bintree{nil, map[string]*bintree{ "wordlist": &bintree{nil, map[string]*bintree{ "chinese_simplified.txt": &bintree{keysWordlistChinese_simplifiedTxt, map[string]*bintree{}}, - "english.txt": &bintree{keysWordlistEnglishTxt, map[string]*bintree{}}, - "japanese.txt": &bintree{keysWordlistJapaneseTxt, map[string]*bintree{}}, - "spanish.txt": &bintree{keysWordlistSpanishTxt, map[string]*bintree{}}, + "english.txt": &bintree{keysWordlistEnglishTxt, map[string]*bintree{}}, + "japanese.txt": &bintree{keysWordlistJapaneseTxt, map[string]*bintree{}}, + "spanish.txt": &bintree{keysWordlistSpanishTxt, map[string]*bintree{}}, }}, }}, }} @@ -305,4 +306,3 @@ func _filePath(dir, name string) string { cannonicalName := strings.Replace(name, "\\", "/", -1) return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) } - 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{} diff --git a/version.go b/version.go index 23d065446..585d7e0d7 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ package crypto -const Version = "0.3.0" +const Version = "0.4.0"