diff --git a/.gitignore b/.gitignore index 381931381..f37225baa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.swp *.swo +vendor diff --git a/Makefile b/Makefile index 0cd05448a..59440fe66 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,27 @@ -.PHONY: docs +.PHONEY: all docs test install get_vendor_deps ensure_tools + +GOTOOLS = \ + github.com/Masterminds/glide REPO:=github.com/tendermint/go-crypto docs: @go get github.com/davecheney/godoc2md godoc2md $(REPO) > README.md +all: install test + +install: + go install ./cmd/keys + test: - go test ./... + go test `glide novendor` + +get_vendor_deps: ensure_tools + @rm -rf vendor/ + @echo "--> Running glide install" + @glide install + +ensure_tools: + go get $(GOTOOLS) + + diff --git a/armor.go b/armor.go index 9343284e1..3d2eff5e7 100644 --- a/armor.go +++ b/armor.go @@ -4,7 +4,7 @@ import ( "bytes" "io/ioutil" - . "github.com/tendermint/go-common" + . "github.com/tendermint/tmlibs/common" "golang.org/x/crypto/openpgp/armor" ) diff --git a/circle.yml b/circle.yml new file mode 100644 index 000000000..23ac4bd9f --- /dev/null +++ b/circle.yml @@ -0,0 +1,21 @@ +machine: + environment: + GOPATH: /home/ubuntu/.go_workspace + PROJECT_PARENT_PATH: "$GOPATH/src/github.com/$CIRCLE_PROJECT_USERNAME" + PROJECT_PATH: $GOPATH/src/github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME + GO15VENDOREXPERIMENT: 1 + hosts: + circlehost: 127.0.0.1 + localhost: 127.0.0.1 + +dependencies: + override: + - mkdir -p "$PROJECT_PARENT_PATH" + - ln -sf "$HOME/$CIRCLE_PROJECT_REPONAME/" "$PROJECT_PATH" + post: + - go version + +test: + override: + - "go version" + - "cd $PROJECT_PATH && make get_vendor_deps && make test" diff --git a/cmd/README.md b/cmd/README.md new file mode 100644 index 000000000..8bf9ca73b --- /dev/null +++ b/cmd/README.md @@ -0,0 +1,117 @@ +# Keys CLI + +This is as much an example how to expose cobra/viper, as for a cli itself +(I think this code is overkill for what go-keys needs). But please look at +the commands, and give feedback and changes. + +`RootCmd` calls some initialization functions (`cobra.OnInitialize` and `RootCmd.PersistentPreRunE`) which serve to connect environmental variables and cobra flags, as well as load the config file. It also validates the flags registered on root and creates the cryptomanager, which will be used by all subcommands. + +## Help info + +``` +# keys help + +Keys allows you to manage your local keystore for tendermint. + +These keys may be in any format supported by go-crypto and can be +used by light-clients, full nodes, or any other application that +needs to sign with a private key. + +Usage: + keys [command] + +Available Commands: + get Get details of one key + list List all keys + new Create a new public/private key pair + serve Run the key manager as an http server + update Change the password for a private key + +Flags: + --keydir string Directory to store private keys (subdir of root) (default "keys") + -o, --output string Output format (text|json) (default "text") + -r, --root string root directory for config and data (default "/Users/ethan/.tlc") + +Use "keys [command] --help" for more information about a command. +``` + +## Getting the config file + +The first step is to load in root, by checking the following in order: + +* -r, --root command line flag +* TM_ROOT environmental variable +* default ($HOME/.tlc evaluated at runtime) + +Once the `rootDir` is established, the script looks for a config file named `keys.{json,toml,yaml,hcl}` in that directory and parses it. These values will provide defaults for flags of the same name. + +There is an example config file for testing out locally, which writes keys to `./.mykeys`. You can + +## Getting/Setting variables + +When we want to get the value of a user-defined variable (eg. `output`), we can call `viper.GetString("output")`, which will do the following checks, until it finds a match: + +* Is `--output` command line flag present? +* Is `TM_OUTPUT` environmental variable set? +* Was a config file found and does it have an `output` variable? +* Is there a default set on the command line flag? + +If no variable is set and there was no default, we get back "". + +This setup allows us to have powerful command line flags, but use env variables or config files (local or 12-factor style) to avoid passing these arguments every time. + +## Nesting structures + +Sometimes we don't just need key-value pairs, but actually a multi-level config file, like + +``` +[mail] +from = "no-reply@example.com" +server = "mail.example.com" +port = 567 +password = "XXXXXX" +``` + +This CLI is too simple to warant such a structure, but I think eg. tendermint could benefit from such an approach. Here are some pointers: + +* [Accessing nested keys from config files](https://github.com/spf13/viper#accessing-nested-keys) +* [Overriding nested values with envvars](https://www.netlify.com/blog/2016/09/06/creating-a-microservice-boilerplate-in-go/#nested-config-values) - the mentioned outstanding PR is already merged into master! +* Overriding nested values with cli flags? (use `--log_config.level=info` ??) + +I'd love to see an example of this fully worked out in a more complex CLI. + +## Have your cake and eat it too + +It's easy to render data different ways. Some better for viewing, some better for importing to other programs. You can just add some global (persistent) flags to control the output formatting, and everyone gets what they want. + +``` +# keys list -e hex +All keys: +betty d0789984492b1674e276b590d56b7ae077f81adc +john b77f4720b220d1411a649b6c7f1151eb6b1c226a + +# keys list -e btc +All keys: +betty 3uTF4r29CbtnzsNHZoPSYsE4BDwH +john 3ZGp2Md35iw4XVtRvZDUaAEkCUZP + +# keys list -e b64 -o json +[ + { + "name": "betty", + "address": "0HiZhEkrFnTidrWQ1Wt64Hf4Gtw=", + "pubkey": { + "type": "secp256k1", + "data": "F83WvhT0KwttSoqQqd_0_r2ztUUaQix5EXdO8AZyREoV31Og780NW59HsqTAb2O4hZ-w-j0Z-4b2IjfdqqfhVQ==" + } + }, + { + "name": "john", + "address": "t39HILIg0UEaZJtsfxFR62scImo=", + "pubkey": { + "type": "ed25519", + "data": "t1LFmbg_8UTwj-n1wkqmnTp6NfaOivokEhlYySlGYCY=" + } + } +] +``` diff --git a/cmd/common.go b/cmd/common.go new file mode 100644 index 000000000..c68cd4304 --- /dev/null +++ b/cmd/common.go @@ -0,0 +1,120 @@ +package cmd + +import ( + "fmt" + "os" + "strings" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/tendermint/go-data/base58" + data "github.com/tendermint/go-wire/data" +) + +/******* + +TODO + +This file should move into go-common or the like as a basis for all cli tools. +It is here for experimentation of re-use between go-keys and light-client. + +*********/ + +const ( + RootFlag = "root" + OutputFlag = "output" + EncodingFlag = "encoding" +) + +func PrepareMainCmd(cmd *cobra.Command, envPrefix, defautRoot string) func() { + cobra.OnInitialize(func() { initEnv(envPrefix) }) + cmd.PersistentFlags().StringP(RootFlag, "r", defautRoot, "root directory for config and data") + cmd.PersistentFlags().StringP(EncodingFlag, "e", "hex", "Binary encoding (hex|b64|btc)") + cmd.PersistentFlags().StringP(OutputFlag, "o", "text", "Output format (text|json)") + cmd.PersistentPreRunE = multiE(bindFlags, setEncoding, validateOutput, cmd.PersistentPreRunE) + return func() { execute(cmd) } +} + +// initEnv sets to use ENV variables if set. +func initEnv(prefix string) { + // env variables with TM prefix (eg. TM_ROOT) + viper.SetEnvPrefix(prefix) + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + viper.AutomaticEnv() +} + +// execute adds all child commands to the root command sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func execute(cmd *cobra.Command) { + if err := cmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(-1) + } +} + +type wrapE func(cmd *cobra.Command, args []string) error + +func multiE(fs ...wrapE) wrapE { + return func(cmd *cobra.Command, args []string) error { + for _, f := range fs { + if f != nil { + if err := f(cmd, args); err != nil { + return err + } + } + } + return nil + } +} + +func bindFlags(cmd *cobra.Command, args []string) error { + // cmd.Flags() includes flags from this command and all persistent flags from the parent + if err := viper.BindPFlags(cmd.Flags()); err != nil { + return err + } + + // rootDir is command line flag, env variable, or default $HOME/.tlc + rootDir := viper.GetString("root") + viper.SetConfigName("config") // name of config file (without extension) + viper.AddConfigPath(rootDir) // search root directory + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + // stderr, so if we redirect output to json file, this doesn't appear + // fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) + } else if _, ok := err.(viper.ConfigFileNotFoundError); !ok { + // we ignore not found error, only parse error + // stderr, so if we redirect output to json file, this doesn't appear + fmt.Fprintf(os.Stderr, "%#v", err) + } + return nil +} + +// setEncoding reads the encoding flag +func setEncoding(cmd *cobra.Command, args []string) error { + // validate and set encoding + enc := viper.GetString("encoding") + switch enc { + case "hex": + data.Encoder = data.HexEncoder + case "b64": + data.Encoder = data.B64Encoder + case "btc": + data.Encoder = base58.BTCEncoder + default: + return errors.Errorf("Unsupported encoding: %s", enc) + } + return nil +} + +func validateOutput(cmd *cobra.Command, args []string) error { + // validate output format + output := viper.GetString(OutputFlag) + switch output { + case "text", "json": + default: + return errors.Errorf("Unsupported output format: %s", output) + } + return nil +} diff --git a/cmd/get.go b/cmd/get.go new file mode 100644 index 000000000..9b8718996 --- /dev/null +++ b/cmd/get.go @@ -0,0 +1,47 @@ +// Copyright © 2017 Ethan Frey +// +// 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 cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// getCmd represents the get command +var getCmd = &cobra.Command{ + Use: "get ", + Short: "Get details of one key", + Long: `Return public details of one local key.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 1 || len(args[0]) == 0 { + fmt.Println("You must provide a name for the key") + return + } + name := args[0] + + info, err := GetKeyManager().Get(name) + if err != nil { + fmt.Println(err.Error()) + return + } + + printInfo(info) + }, +} + +func init() { + RootCmd.AddCommand(getCmd) +} diff --git a/cmd/keys/main.go b/cmd/keys/main.go new file mode 100644 index 000000000..8dd8d6505 --- /dev/null +++ b/cmd/keys/main.go @@ -0,0 +1,27 @@ +// Copyright © 2017 Ethan Frey +// +// 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 main + +import ( + "os" + + "github.com/tendermint/go-crypto/cmd" +) + +func main() { + cmd.PrepareMainCmd(cmd.RootCmd, "TM", os.ExpandEnv("$HOME/.tlc")) + cmd.RootCmd.Execute() + // exec() +} diff --git a/cmd/list.go b/cmd/list.go new file mode 100644 index 000000000..875520159 --- /dev/null +++ b/cmd/list.go @@ -0,0 +1,42 @@ +// Copyright © 2017 Ethan Frey +// +// 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 cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// listCmd represents the list command +var listCmd = &cobra.Command{ + Use: "list", + Short: "List all keys", + Long: `Return a list of all public keys stored by this key manager +along with their associated name and address.`, + Run: func(cmd *cobra.Command, args []string) { + infos, err := GetKeyManager().List() + if err != nil { + fmt.Println(err.Error()) + return + } + + printInfos(infos) + }, +} + +func init() { + RootCmd.AddCommand(listCmd) +} diff --git a/cmd/new.go b/cmd/new.go new file mode 100644 index 000000000..b59874bb0 --- /dev/null +++ b/cmd/new.go @@ -0,0 +1,60 @@ +// Copyright © 2017 Ethan Frey +// +// 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 cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// newCmd represents the new command +var newCmd = &cobra.Command{ + Use: "new ", + Short: "Create a new public/private key pair", + Long: `Add a public/private key pair to the key store. +The password muts be entered in the terminal and not +passed as a command line argument for security.`, + Run: newPassword, +} + +func init() { + RootCmd.AddCommand(newCmd) + newCmd.Flags().StringP("type", "t", "ed25519", "Type of key (ed25519|secp256k1)") +} + +func newPassword(cmd *cobra.Command, args []string) { + if len(args) != 1 || len(args[0]) == 0 { + fmt.Println("You must provide a name for the key") + return + } + name := args[0] + algo := viper.GetString("type") + + pass, err := getCheckPassword("Enter a passphrase:", "Repeat the passphrase:") + if err != nil { + fmt.Println(err.Error()) + return + } + + info, err := GetKeyManager().Create(name, pass, algo) + if err != nil { + fmt.Println(err.Error()) + return + } + + printInfo(info) +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 000000000..401e7ec38 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,57 @@ +// Copyright © 2017 Ethan Frey +// +// 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 cmd + +import ( + "path/filepath" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + keys "github.com/tendermint/go-crypto/keys" + "github.com/tendermint/go-crypto/keys/cryptostore" + "github.com/tendermint/go-crypto/keys/storage/filestorage" +) + +const KeySubdir = "keys" + +var ( + manager keys.Manager +) + +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ + Use: "keys", + Short: "Key manager for tendermint clients", + Long: `Keys allows you to manage your local keystore for tendermint. + +These keys may be in any format supported by go-crypto and can be +used by light-clients, full nodes, or any other application that +needs to sign with a private key.`, +} + +// GetKeyManager initializes a key manager based on the configuration +func GetKeyManager() keys.Manager { + if manager == nil { + // store the keys directory + rootDir := viper.GetString("root") + keyDir := filepath.Join(rootDir, KeySubdir) + // and construct the key manager + manager = cryptostore.New( + cryptostore.SecretBox, + filestorage.New(keyDir), + ) + } + return manager +} diff --git a/cmd/serve.go b/cmd/serve.go new file mode 100644 index 000000000..5ea96db3f --- /dev/null +++ b/cmd/serve.go @@ -0,0 +1,106 @@ +// Copyright © 2017 Ethan Frey +// +// 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 cmd + +import ( + "fmt" + "net" + "net/http" + "os" + + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/tendermint/go-crypto/keys/server" +) + +// serveCmd represents the serve command +var serveCmd = &cobra.Command{ + Use: "serve", + Short: "Run the key manager as an http server", + Long: `Launch an http server with a rest api to manage the +private keys much more in depth than the cli can perform. +In particular, this will allow you to sign transactions with +the private keys in the store.`, + RunE: serveHTTP, +} + +func init() { + RootCmd.AddCommand(serveCmd) + serveCmd.Flags().IntP("port", "p", 8118, "TCP Port for listen for http server") + serveCmd.Flags().StringP("socket", "s", "", "UNIX socket for more secure http server") + serveCmd.Flags().StringP("type", "t", "ed25519", "Default key type (ed25519|secp256k1)") +} + +func serveHTTP(cmd *cobra.Command, args []string) error { + var l net.Listener + var err error + socket := viper.GetString("socket") + if socket != "" { + l, err = createSocket(socket) + if err != nil { + return errors.Wrap(err, "Cannot create socket") + } + } else { + port := viper.GetInt("port") + l, err = net.Listen("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + return errors.Errorf("Cannot listen on port %d", port) + } + } + + router := mux.NewRouter() + ks := server.New(GetKeyManager(), viper.GetString("type")) + ks.Register(router) + + // only set cors for tcp listener + var h http.Handler + if socket == "" { + allowedHeaders := handlers.AllowedHeaders([]string{"Content-Type"}) + h = handlers.CORS(allowedHeaders)(router) + } else { + h = router + } + + err = http.Serve(l, h) + fmt.Printf("Server Killed: %+v\n", err) + return nil +} + +// createSocket deletes existing socket if there, creates a new one, +// starts a server on the socket, and sets permissions to 0600 +func createSocket(socket string) (net.Listener, error) { + err := os.Remove(socket) + if err != nil && !os.IsNotExist(err) { + // only fail if it does exist and cannot be deleted + return nil, err + } + + l, err := net.Listen("unix", socket) + if err != nil { + return nil, err + } + + mode := os.FileMode(0700) | os.ModeSocket + err = os.Chmod(socket, mode) + if err != nil { + l.Close() + return nil, err + } + + return l, nil +} diff --git a/cmd/update.go b/cmd/update.go new file mode 100644 index 000000000..3835242c5 --- /dev/null +++ b/cmd/update.go @@ -0,0 +1,59 @@ +// Copyright © 2017 Ethan Frey +// +// 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 cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// updateCmd represents the update command +var updateCmd = &cobra.Command{ + Use: "update ", + Short: "Change the password for a private key", + Long: `Change the password for a private key.`, + Run: updatePassword, +} + +func init() { + RootCmd.AddCommand(updateCmd) +} + +func updatePassword(cmd *cobra.Command, args []string) { + if len(args) != 1 || len(args[0]) == 0 { + fmt.Println("You must provide a name for the key") + return + } + name := args[0] + + oldpass, err := getPassword("Enter the current passphrase:") + if err != nil { + fmt.Println(err.Error()) + return + } + newpass, err := getCheckPassword("Enter the new passphrase:", "Repeat the new passphrase:") + if err != nil { + fmt.Println(err.Error()) + return + } + + err = GetKeyManager().Update(name, oldpass, newpass) + if err != nil { + fmt.Println(err.Error()) + } else { + fmt.Println("Password successfully updated!") + } +} diff --git a/cmd/utils.go b/cmd/utils.go new file mode 100644 index 000000000..a75262676 --- /dev/null +++ b/cmd/utils.go @@ -0,0 +1,77 @@ +package cmd + +import ( + "fmt" + + "github.com/bgentry/speakeasy" + "github.com/pkg/errors" + "github.com/spf13/viper" + keys "github.com/tendermint/go-crypto/keys" + data "github.com/tendermint/go-wire/data" +) + +const PassLength = 10 + +func getPassword(prompt string) (string, error) { + pass, err := speakeasy.Ask(prompt) + if err != nil { + return "", err + } + if len(pass) < PassLength { + return "", errors.Errorf("Password must be at least %d characters", PassLength) + } + return pass, nil +} + +func getCheckPassword(prompt, prompt2 string) (string, error) { + // TODO: own function??? + pass, err := getPassword(prompt) + if err != nil { + return "", err + } + pass2, err := getPassword(prompt2) + if err != nil { + return "", err + } + if pass != pass2 { + return "", errors.New("Passphrases don't match") + } + return pass, nil +} + +func printInfo(info keys.Info) { + switch viper.Get(OutputFlag) { + case "text": + addr, err := data.ToText(info.Address) + if err != nil { + panic(err) // really shouldn't happen... + } + sep := "\t\t" + if len(info.Name) > 7 { + sep = "\t" + } + fmt.Printf("%s%s%s\n", info.Name, sep, addr) + case "json": + json, err := data.ToJSON(info) + if err != nil { + panic(err) // really shouldn't happen... + } + fmt.Println(string(json)) + } +} + +func printInfos(infos keys.Infos) { + switch viper.Get(OutputFlag) { + case "text": + fmt.Println("All keys:") + for _, i := range infos { + printInfo(i) + } + case "json": + json, err := data.ToJSON(infos) + if err != nil { + panic(err) // really shouldn't happen... + } + fmt.Println(string(json)) + } +} diff --git a/crypto.go b/crypto.go new file mode 100644 index 000000000..d9caf1ad9 --- /dev/null +++ b/crypto.go @@ -0,0 +1,9 @@ +package crypto + +// Types of implementations +const ( + TypeEd25519 = byte(0x01) + TypeSecp256k1 = byte(0x02) + NameEd25519 = "ed25519" + NameSecp256k1 = "secp256k1" +) diff --git a/embed_test.go b/embed_test.go index 71852848e..e2d2fe504 100644 --- a/embed_test.go +++ b/embed_test.go @@ -6,53 +6,16 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - data "github.com/tendermint/go-data" + data "github.com/tendermint/go-wire/data" ) -type Foo struct { - Name string -} - -func (f Foo) Greet() string { - return "Foo: " + f.Name -} - -type Bar struct { - Age int -} - -func (b Bar) Greet() string { - return fmt.Sprintf("Bar #%d", b.Age) +type PubName struct { + PubNameInner } type PubNameInner interface { - Greet() string -} - -type privNameInner interface { - Greet() string -} - -type Greeter interface { - Greet() string -} - -var ( - pubNameMapper, privNameMapper data.Mapper -) - -// register both public key types with go-data (and thus go-wire) -func init() { - pubNameMapper = data.NewMapper(PubName{}). - RegisterImplementation(Foo{}, "foo", 1). - RegisterImplementation(Bar{}, "bar", 2) - privNameMapper = data.NewMapper(PrivName{}). - RegisterImplementation(Foo{}, "foo", 1). - RegisterImplementation(Bar{}, "bar", 2) -} - -type PubName struct { - PubNameInner + AssertIsPubNameInner() + String() string } func (p PubName) MarshalJSON() ([]byte, error) { @@ -67,59 +30,61 @@ func (p *PubName) UnmarshalJSON(data []byte) error { return err } -type PrivName struct { - privNameInner -} +var pubNameMapper = data.NewMapper(PubName{}). + RegisterImplementation(PubNameFoo{}, "foo", 1). + RegisterImplementation(PubNameBar{}, "bar", 2) + +func (f PubNameFoo) AssertIsPubNameInner() {} +func (f PubNameBar) AssertIsPubNameInner() {} -func (p PrivName) MarshalJSON() ([]byte, error) { - return privNameMapper.ToJSON(p.privNameInner) +//---------------------------------------- + +type PubNameFoo struct { + Name string } -func (p *PrivName) UnmarshalJSON(data []byte) error { - parsed, err := privNameMapper.FromJSON(data) - if err == nil && parsed != nil { - p.privNameInner = parsed.(privNameInner) - } - return err +func (f PubNameFoo) String() string { return "Foo: " + f.Name } + +type PubNameBar struct { + Age int } +func (b PubNameBar) String() string { return fmt.Sprintf("Bar #%d", b.Age) } + +//---------------------------------------- + // TestEncodeDemo tries the various strategies to encode the objects func TestEncodeDemo(t *testing.T) { assert, require := assert.New(t), require.New(t) - // assert := assert.New(t) - // require := require.New(t) cases := []struct { - in, out Greeter + in, out PubNameInner expected string }{ - {PubName{Foo{"pub-foo"}}, &PubName{}, "Foo: pub-foo"}, - {PubName{Bar{7}}, &PubName{}, "Bar #7"}, - // Note these fail - if you can figure a solution here, I'll buy you a beer :) - // {PrivName{Foo{"priv-foo"}}, &PrivName{}, "Foo: priv-foo"}, - // {PrivName{Bar{9}}, &PrivName{}, "Bar #9"}, + {PubName{PubNameFoo{"pub-foo"}}, &PubName{}, "Foo: pub-foo"}, + {PubName{PubNameBar{7}}, &PubName{}, "Bar #7"}, } for i, tc := range cases { - // make sure it is proper to start - require.Equal(tc.expected, tc.in.Greet()) - fmt.Println(tc.expected) - // now, try to encode as binary + // Make sure it is proper to start + require.Equal(tc.expected, tc.in.String()) + + // 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) { - assert.Equal(tc.expected, tc.out.Greet()) + assert.Equal(tc.expected, tc.out.String()) } } - // try to encode it as json + // 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) { - assert.Equal(tc.expected, tc.out.Greet()) + assert.Equal(tc.expected, tc.out.String()) } } } diff --git a/encode_test.go b/encode_test.go index 977cc64cf..6c5d03a1d 100644 --- a/encode_test.go +++ b/encode_test.go @@ -1,12 +1,14 @@ package crypto import ( + "fmt" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - data "github.com/tendermint/go-data" + data "github.com/tendermint/go-wire/data" + wire "github.com/tendermint/go-wire" ) type byter interface { @@ -14,13 +16,15 @@ type byter interface { } // go to wire encoding and back -func checkWire(t *testing.T, in byter, reader interface{}, typ byte) { +func checkWire(t *testing.T, in byter, reader interface{}, typ byte, size int) { // test to and from binary bin, err := data.ToWire(in) require.Nil(t, err, "%+v", err) assert.Equal(t, typ, bin[0]) // make sure this is compatible with current (Bytes()) encoding assert.Equal(t, in.Bytes(), bin) + // make sure we have the expected length + assert.Equal(t, size, len(bin)) err = data.FromWire(bin, reader) require.Nil(t, err, "%+v", err) @@ -48,41 +52,61 @@ func checkJSON(t *testing.T, in interface{}, reader interface{}, typ string) { assert.True(t, strings.Contains(string(js), parts[1])) } +// make sure go-wire json can still figure this out... +func checkWireJSON(t *testing.T, in interface{}, reader interface{}, typ byte) { + // test to and from binary + var err error + js := wire.JSONBytes(in) + btyp := fmt.Sprintf("[%d,", typ) + assert.True(t, strings.HasPrefix(string(js), btyp), string(js)) + + wire.ReadJSON(reader, js, &err) + require.Nil(t, err, "%+v", err) +} + func TestKeyEncodings(t *testing.T) { cases := []struct { - privKey PrivKeyS + privKey PrivKey keyType byte keyName string + // 1 (type byte) + size of byte array + privSize, pubSize int }{ { - privKey: PrivKeyS{GenPrivKeyEd25519()}, - keyType: TypeEd25519, - keyName: NameEd25519, + privKey: GenPrivKeyEd25519().Wrap(), + keyType: TypeEd25519, + keyName: NameEd25519, + privSize: 65, + pubSize: 33, }, { - privKey: PrivKeyS{GenPrivKeySecp256k1()}, - keyType: TypeSecp256k1, - keyName: NameSecp256k1, + privKey: GenPrivKeySecp256k1().Wrap(), + keyType: TypeSecp256k1, + keyName: NameSecp256k1, + privSize: 33, + pubSize: 34, }, } for _, tc := range cases { // check (de/en)codings of private key - priv2 := PrivKeyS{} - checkWire(t, tc.privKey, &priv2, tc.keyType) + var priv2, priv3, priv4 PrivKey + checkWire(t, tc.privKey, &priv2, tc.keyType, tc.privSize) assert.EqualValues(t, tc.privKey, priv2) - priv3 := PrivKeyS{} checkJSON(t, tc.privKey, &priv3, tc.keyName) assert.EqualValues(t, tc.privKey, priv3) + checkWireJSON(t, tc.privKey, &priv4, tc.keyType) + assert.EqualValues(t, tc.privKey, priv4) // check (de/en)codings of public key - pubKey := PubKeyS{tc.privKey.PubKey()} - pub2 := PubKeyS{} - checkWire(t, pubKey, &pub2, tc.keyType) + pubKey := tc.privKey.PubKey() + var pub2, pub3, pub4 PubKey + checkWire(t, pubKey, &pub2, tc.keyType, tc.pubSize) assert.EqualValues(t, pubKey, pub2) - pub3 := PubKeyS{} checkJSON(t, pubKey, &pub3, tc.keyName) assert.EqualValues(t, pubKey, pub3) + checkWireJSON(t, pubKey, &pub4, tc.keyType) + assert.EqualValues(t, pubKey, pub4) } } @@ -95,18 +119,65 @@ func toFromJSON(t *testing.T, in interface{}, recvr interface{}) { func TestNilEncodings(t *testing.T) { // make sure sigs are okay with nil - a, b := SignatureS{}, SignatureS{} + var a, b Signature toFromJSON(t, a, &b) assert.EqualValues(t, a, b) // make sure sigs are okay with nil - c, d := PubKeyS{}, PubKeyS{} + var c, d PubKey toFromJSON(t, c, &d) assert.EqualValues(t, c, d) // make sure sigs are okay with nil - e, f := PrivKeyS{}, PrivKeyS{} + var e, f PrivKey toFromJSON(t, e, &f) assert.EqualValues(t, e, f) } + +type SigMessage struct { + Key PubKey + Sig Signature +} + +func (s SigMessage) Bytes() []byte { + return wire.BinaryBytes(s) +} + +func TestEmbededWireEncodings(t *testing.T) { + assert := assert.New(t) + + cases := []struct { + privKey PrivKey + keyType byte + keyName string + size int // pub + sig size + }{ + { + privKey: GenPrivKeyEd25519().Wrap(), + keyType: TypeEd25519, + keyName: NameEd25519, + size: 2 + 32 + 64, + }, + // { + // privKey: GenPrivKeySecp256k1().Wrap(), + // keyType: TypeSecp256k1, + // keyName: NameSecp256k1, + // size: 2 + 33 + 72, // ugh, either 72 or 73 depending.... + // }, + } + + payload := randBytes(20) + for i, tc := range cases { + pubKey := tc.privKey.PubKey() + sig := tc.privKey.Sign(payload) + assert.True(pubKey.VerifyBytes(payload, sig), "%d", i) + + msg := SigMessage{ + Key: pubKey, + Sig: sig, + } + var msg2 SigMessage + checkWire(t, msg, &msg2, tc.keyType, tc.size) + } +} diff --git a/glide.lock b/glide.lock new file mode 100644 index 000000000..b804b4801 --- /dev/null +++ b/glide.lock @@ -0,0 +1,148 @@ +hash: 8a63c035134ec024df64d8cc43a732712e48e4cfc5de30d45c1b692b3e9a75b8 +updated: 2017-04-19T17:06:49.640329917+02:00 +imports: +- name: github.com/bgentry/speakeasy + version: 675b82c74c0ed12283ee81ba8a534c8982c07b85 +- name: github.com/btcsuite/btcd + version: 583684b21bfbde9b5fc4403916fd7c807feb0289 + subpackages: + - btcec + - chaincfg + - chaincfg/chainhash + - wire +- name: github.com/btcsuite/btcutil + version: a5ecb5d9547afe8d1672073dbdc348203de744a0 + subpackages: + - base58 + - hdkeychain +- name: github.com/btcsuite/golangcrypto + version: 53f62d9b43e87a6c56975cf862af7edf33a8d0df + subpackages: + - ripemd160 +- name: github.com/fsnotify/fsnotify + version: 4da3e2cfbabc9f751898f250b49f2439785783a1 +- name: github.com/go-playground/locales + version: 084b0226cf88d891a2bdeccac01d592af13a8f7b + subpackages: + - currency +- name: github.com/go-playground/universal-translator + version: b32fa301c9fe55953584134cb6853a13c87ec0a1 +- name: github.com/go-stack/stack + version: 100eb0c0a9c5b306ca2fb4f165df21d80ada4b82 +- name: github.com/gorilla/context + version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 +- name: github.com/gorilla/handlers + version: 3a5767ca75ece5f7f1440b1d16975247f8d8b221 +- name: github.com/gorilla/mux + version: 392c28fe23e1c45ddba891b0320b3b5df220beea +- name: github.com/hashicorp/hcl + version: 630949a3c5fa3c613328e1b8256052cbc2327c9b + subpackages: + - hcl/ast + - hcl/parser + - hcl/scanner + - hcl/strconv + - hcl/token + - json/parser + - json/scanner + - json/token +- name: github.com/inconshreveable/mousetrap + version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 +- name: github.com/magiconair/properties + version: 51463bfca2576e06c62a8504b5c0f06d61312647 +- name: github.com/mattn/go-colorable + version: a392f450ea64cee2b268dfaacdc2502b50a22b18 +- name: github.com/mattn/go-isatty + version: 57fdcb988a5c543893cc61bce354a6e24ab70022 +- name: github.com/mitchellh/mapstructure + version: 53818660ed4955e899c0bcafa97299a388bd7c8e +- name: github.com/pelletier/go-buffruneio + version: c37440a7cf42ac63b919c752ca73a85067e05992 +- name: github.com/pelletier/go-toml + version: 13d49d4606eb801b8f01ae542b4afc4c6ee3d84a +- name: github.com/pkg/errors + version: bfd5150e4e41705ded2129ec33379de1cb90b513 +- name: github.com/spf13/afero + version: 9be650865eab0c12963d8753212f4f9c66cdcf12 + subpackages: + - mem +- name: github.com/spf13/cast + version: acbeb36b902d72a7a4c18e8f3241075e7ab763e4 +- name: github.com/spf13/cobra + version: fcd0c5a1df88f5d6784cb4feead962c3f3d0b66c +- name: github.com/spf13/jwalterweatherman + version: fa7ca7e836cf3a8bb4ebf799f472c12d7e903d66 +- name: github.com/spf13/pflag + version: 9ff6c6923cfffbcd502984b8e0c80539a94968b7 +- name: github.com/spf13/viper + version: 5d46e70da8c0b6f812e0b170b7a985753b5c63cb +- name: github.com/tendermint/ed25519 + version: 1f52c6f8b8a5c7908aff4497c186af344b428925 + subpackages: + - edwards25519 + - extra25519 +- name: github.com/tendermint/go-data + version: e7fcc6d081ec8518912fcdc103188275f83a3ee5 + subpackages: + - base58 +- name: github.com/tendermint/go-wire + version: 9127836cbb6dd99e020cb840a0cedcedc4671468 + subpackages: + - data + - data/base58 +- name: github.com/tendermint/log15 + version: ae0f3d6450da9eac7074b439c8e1c3cabf0d5ce6 + subpackages: + - term +- name: github.com/tendermint/tmlibs + version: 2f8551d3b614dd0c0c6c114c42ab25901cc41a52 + subpackages: + - common + - logger +- name: golang.org/x/crypto + version: 728b753d0135da6801d45a38e6f43ff55779c5c2 + subpackages: + - bcrypt + - blowfish + - nacl/secretbox + - openpgp/armor + - openpgp/errors + - pbkdf2 + - poly1305 + - ripemd160 + - salsa20/salsa +- name: golang.org/x/sys + version: 99f16d856c9836c42d24e7ab64ea72916925fa97 + subpackages: + - unix +- name: golang.org/x/text + version: f4b4367115ec2de254587813edaa901bc1c723a8 + subpackages: + - transform + - unicode/norm +- name: gopkg.in/go-playground/validator.v9 + version: 4bd19358521c53f09639f21e2a9d6883d6890f24 +- name: gopkg.in/yaml.v2 + version: cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b +testImports: +- name: github.com/cmars/basen + version: fe3947df716ebfda9847eb1b9a48f9592e06478c +- name: github.com/davecgh/go-spew + version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9 + subpackages: + - spew +- name: github.com/mndrix/btcutil + version: d3a63a5752ecf3fbc06bd97365da752111c263df +- name: github.com/pmezard/go-difflib + version: d8ed2627bdf02c080bf22230dbb337003b7aba2d + subpackages: + - difflib +- name: github.com/stretchr/testify + version: 69483b4bd14f5845b5a1e55bca19e954e827f1d0 + subpackages: + - assert + - require +- name: github.com/tyler-smith/go-bip32 + version: eb790af526c30f23a7c8b00a48e342f9d0bd6386 +- name: github.com/tyler-smith/go-bip39 + version: 8e7a99b3e716f36d3b080a9a70f9eb45abe4edcc diff --git a/glide.yaml b/glide.yaml new file mode 100644 index 000000000..2566feef9 --- /dev/null +++ b/glide.yaml @@ -0,0 +1,40 @@ +package: github.com/tendermint/go-crypto +import: +- package: github.com/btcsuite/btcd + subpackages: + - btcec +- package: github.com/btcsuite/btcutil + subpackages: + - base58 +- package: github.com/tendermint/ed25519 + subpackages: + - extra25519 +- package: github.com/tendermint/tmlibs + version: unstable +- package: github.com/tendermint/go-wire + version: unstable + subpackages: + - data + - data/base58 +- package: golang.org/x/crypto + subpackages: + - blowfish + - 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 +testImport: +- package: github.com/mndrix/btcutil +- package: github.com/stretchr/testify + version: ^1.1.4 + subpackages: + - assert + - require +- package: github.com/tyler-smith/go-bip32 +- package: github.com/tyler-smith/go-bip39 diff --git a/hd/address.go b/hd/address.go index afdeefbc4..28f70a98f 100644 --- a/hd/address.go +++ b/hd/address.go @@ -22,21 +22,13 @@ import ( "golang.org/x/crypto/ripemd160" ) -const ( - // BIP32 chainpath prefix - CHAINPATH_PREFIX_DEPOSIT = 0 - CHAINPATH_PREFIX_CHANGE = 1 - CHAINPATH_PREFIX_SWEEP = 2 - CHAINPATH_PREFIX_SWEEP_DRY = 102 -) - -func ComputeAddress(coin string, pubKeyHex string, chainHex string, path string, index int32) string { +func ComputeAddress(pubKeyHex string, chainHex string, path string, index int32) string { pubKeyBytes := DerivePublicKeyForPath( HexDecode(pubKeyHex), HexDecode(chainHex), fmt.Sprintf("%v/%v", path, index), ) - return AddrFromPubKeyBytes(coin, pubKeyBytes) + return AddrFromPubKeyBytes(pubKeyBytes) } func ComputePrivateKey(mprivHex string, chainHex string, path string, index int32) string { @@ -48,9 +40,9 @@ func ComputePrivateKey(mprivHex string, chainHex string, path string, index int3 return HexEncode(privKeyBytes) } -func ComputeAddressForPrivKey(coin string, privKey string) string { +func ComputeAddressForPrivKey(privKey string) string { pubKeyBytes := PubKeyBytesFromPrivKeyBytes(HexDecode(privKey), true) - return AddrFromPubKeyBytes(coin, pubKeyBytes) + return AddrFromPubKeyBytes(pubKeyBytes) } func SignMessage(privKey string, message string, compress bool) string { @@ -86,8 +78,8 @@ func ComputeMastersFromSeed(seed string) (string, string, string, string) { return HexEncode(pubKeyBytes), HexEncode(secret), HexEncode(chain), HexEncode(secret) } -func ComputeWIF(coin string, privKey string, compress bool) string { - return WIFFromPrivKeyBytes(coin, HexDecode(privKey), compress) +func ComputeWIF(privKey string, compress bool) string { + return WIFFromPrivKeyBytes(HexDecode(privKey), compress) } func ComputeTxId(rawTxHex string) string { @@ -100,7 +92,7 @@ func printKeyInfo(privKeyBytes []byte, pubKeyBytes []byte, chain []byte) { if pubKeyBytes == nil { pubKeyBytes = PubKeyBytesFromPrivKeyBytes(privKeyBytes, true) } - addr := AddrFromPubKeyBytes("BTC", pubKeyBytes) + addr := AddrFromPubKeyBytes(pubKeyBytes) log.Println("\nprikey:\t%v\npubKeyBytes:\t%v\naddr:\t%v\nchain:\t%v", HexEncode(privKeyBytes), HexEncode(pubKeyBytes), @@ -225,7 +217,8 @@ func I64(key []byte, data []byte) ([]byte, []byte) { return I[:32], I[32:] } -func AddrFromPubKeyBytes(coin string, pubKeyBytes []byte) string { +// This returns a Bitcoin-like address. +func AddrFromPubKeyBytes(pubKeyBytes []byte) string { prefix := byte(0x00) // TODO Make const or configurable h160 := CalcHash160(pubKeyBytes) h160 = append([]byte{prefix}, h160...) @@ -234,7 +227,15 @@ func AddrFromPubKeyBytes(coin string, pubKeyBytes []byte) string { return base58.Encode(b) } -func WIFFromPrivKeyBytes(coin string, privKeyBytes []byte, compress bool) string { +func AddrBytesFromPubKeyBytes(pubKeyBytes []byte) (addrBytes []byte, checksum []byte) { + prefix := byte(0x00) // TODO Make const or configurable + h160 := CalcHash160(pubKeyBytes) + _h160 := append([]byte{prefix}, h160...) + checksum = CalcHash256(_h160)[:4] + return h160, checksum +} + +func WIFFromPrivKeyBytes(privKeyBytes []byte, compress bool) string { prefix := byte(0x80) // TODO Make const or configurable bytes := append([]byte{prefix}, privKeyBytes...) if compress { diff --git a/keys/Makefile b/keys/Makefile new file mode 100644 index 000000000..63eb70787 --- /dev/null +++ b/keys/Makefile @@ -0,0 +1,22 @@ +GOTOOLS = \ + github.com/mitchellh/gox \ + github.com/Masterminds/glide + +.PHONEY: all test install get_vendor_deps ensure_tools + +all: install test + +test: + go test `glide novendor` + +install: + go install ./cmd/keys + +get_vendor_deps: ensure_tools + @rm -rf vendor/ + @echo "--> Running glide install" + @glide install + +ensure_tools: + go get $(GOTOOLS) + diff --git a/keys/cryptostore/docs.go b/keys/cryptostore/docs.go new file mode 100644 index 000000000..5c66fba2d --- /dev/null +++ b/keys/cryptostore/docs.go @@ -0,0 +1,25 @@ +/* +package cryptostore maintains everything needed for doing public-key signing and +key management in software, based on the go-crypto library from tendermint. + +It is flexible, and allows the user to provide a key generation algorithm +(currently Ed25519 or Secp256k1), an encoder to passphrase-encrypt our keys +when storing them (currently SecretBox from NaCl), and a method to persist +the keys (currently FileStorage like ssh, or MemStorage for tests). +It should be relatively simple to write your own implementation of these +interfaces to match your specific security requirements. + +Note that the private keys are never exposed outside the package, and the +interface of Manager could be implemented by an HSM in the future for +enhanced security. It would require a completely different implementation +however. + +This Manager aims to implement Signer and KeyManager interfaces, along +with some extensions to allow importing/exporting keys and updating the +passphrase. + +Encoder and Generator implementations are currently in this package, +keys.Storage implementations exist as subpackages of +keys/storage +*/ +package cryptostore diff --git a/keys/cryptostore/enc_storage.go b/keys/cryptostore/enc_storage.go new file mode 100644 index 000000000..daeb220b5 --- /dev/null +++ b/keys/cryptostore/enc_storage.go @@ -0,0 +1,49 @@ +package cryptostore + +import ( + crypto "github.com/tendermint/go-crypto" + keys "github.com/tendermint/go-crypto/keys" +) + +// encryptedStorage needs passphrase to get private keys +type encryptedStorage struct { + coder Encoder + store keys.Storage +} + +func (es encryptedStorage) Put(name, pass string, key crypto.PrivKey) error { + secret, err := es.coder.Encrypt(key, pass) + if err != nil { + return err + } + + ki := info(name, key) + return es.store.Put(name, secret, ki) +} + +func (es encryptedStorage) Get(name, pass string) (crypto.PrivKey, keys.Info, error) { + secret, info, err := es.store.Get(name) + if err != nil { + return crypto.PrivKey{}, info, err + } + key, err := es.coder.Decrypt(secret, pass) + return key, info, err +} + +func (es encryptedStorage) List() (keys.Infos, error) { + return es.store.List() +} + +func (es encryptedStorage) Delete(name string) error { + return es.store.Delete(name) +} + +// info hardcodes the encoding of keys +func info(name string, key crypto.PrivKey) keys.Info { + pub := key.PubKey() + return keys.Info{ + Name: name, + Address: pub.Address(), + PubKey: pub, + } +} diff --git a/keys/cryptostore/encoder.go b/keys/cryptostore/encoder.go new file mode 100644 index 000000000..12792813c --- /dev/null +++ b/keys/cryptostore/encoder.go @@ -0,0 +1,54 @@ +package cryptostore + +import ( + "github.com/pkg/errors" + crypto "github.com/tendermint/go-crypto" +) + +var ( + // SecretBox uses the algorithm from NaCL to store secrets securely + SecretBox Encoder = secretbox{} + // Noop doesn't do any encryption, should only be used in test code + Noop Encoder = noop{} +) + +// Encoder is used to encrypt any key with a passphrase for storage. +// +// This should use a well-designed symetric encryption algorithm +type Encoder interface { + Encrypt(key crypto.PrivKey, pass string) ([]byte, error) + Decrypt(data []byte, pass string) (crypto.PrivKey, error) +} + +func secret(passphrase string) []byte { + // TODO: Sha256(Bcrypt(passphrase)) + return crypto.Sha256([]byte(passphrase)) +} + +type secretbox struct{} + +func (e secretbox) Encrypt(key crypto.PrivKey, pass string) ([]byte, error) { + s := secret(pass) + cipher := crypto.EncryptSymmetric(key.Bytes(), s) + return cipher, nil +} + +func (e secretbox) Decrypt(data []byte, pass string) (crypto.PrivKey, error) { + s := secret(pass) + private, err := crypto.DecryptSymmetric(data, s) + if err != nil { + return crypto.PrivKey{}, errors.Wrap(err, "Invalid Passphrase") + } + key, err := crypto.PrivKeyFromBytes(private) + return key, errors.Wrap(err, "Invalid Passphrase") +} + +type noop struct{} + +func (n noop) Encrypt(key crypto.PrivKey, pass string) ([]byte, error) { + return key.Bytes(), nil +} + +func (n noop) Decrypt(data []byte, pass string) (crypto.PrivKey, error) { + return crypto.PrivKeyFromBytes(data) +} diff --git a/keys/cryptostore/encoder_test.go b/keys/cryptostore/encoder_test.go new file mode 100644 index 000000000..e5ea21111 --- /dev/null +++ b/keys/cryptostore/encoder_test.go @@ -0,0 +1,59 @@ +package cryptostore_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tendermint/go-crypto/keys/cryptostore" +) + +func TestNoopEncoder(t *testing.T) { + assert, require := assert.New(t), require.New(t) + noop := cryptostore.Noop + + key := cryptostore.GenEd25519.Generate() + key2 := cryptostore.GenSecp256k1.Generate() + + b, err := noop.Encrypt(key, "encode") + require.Nil(err) + assert.NotEmpty(b) + + b2, err := noop.Encrypt(key2, "encode") + require.Nil(err) + assert.NotEmpty(b2) + assert.NotEqual(b, b2) + + // note the decode with a different password works - not secure! + pk, err := noop.Decrypt(b, "decode") + require.Nil(err) + require.NotNil(pk) + assert.Equal(key, pk) + + pk2, err := noop.Decrypt(b2, "kggugougp") + require.Nil(err) + require.NotNil(pk2) + assert.Equal(key2, pk2) +} + +func TestSecretBox(t *testing.T) { + assert, require := assert.New(t), require.New(t) + enc := cryptostore.SecretBox + + key := cryptostore.GenEd25519.Generate() + pass := "some-special-secret" + + b, err := enc.Encrypt(key, pass) + require.Nil(err) + assert.NotEmpty(b) + + // decoding with a different pass is an error + pk, err := enc.Decrypt(b, "decode") + require.NotNil(err) + require.True(pk.Empty()) + + // but decoding with the same passphrase gets us our key + pk, err = enc.Decrypt(b, pass) + require.Nil(err) + assert.Equal(key, pk) +} diff --git a/keys/cryptostore/generator.go b/keys/cryptostore/generator.go new file mode 100644 index 000000000..6bbdb6441 --- /dev/null +++ b/keys/cryptostore/generator.go @@ -0,0 +1,44 @@ +package cryptostore + +import ( + "github.com/pkg/errors" + crypto "github.com/tendermint/go-crypto" +) + +var ( + // GenEd25519 produces Ed25519 private keys + GenEd25519 Generator = GenFunc(genEd25519) + // GenSecp256k1 produces Secp256k1 private keys + GenSecp256k1 Generator = GenFunc(genSecp256) +) + +// Generator determines the type of private key the keystore creates +type Generator interface { + Generate() crypto.PrivKey +} + +// GenFunc is a helper to transform a function into a Generator +type GenFunc func() crypto.PrivKey + +func (f GenFunc) Generate() crypto.PrivKey { + return f() +} + +func genEd25519() crypto.PrivKey { + return crypto.GenPrivKeyEd25519().Wrap() +} + +func genSecp256() crypto.PrivKey { + return crypto.GenPrivKeySecp256k1().Wrap() +} + +func getGenerator(algo string) (Generator, error) { + switch algo { + case crypto.NameEd25519: + return GenEd25519, nil + case crypto.NameSecp256k1: + return GenSecp256k1, nil + default: + return nil, errors.Errorf("Cannot generate keys for algorithm: %s", algo) + } +} diff --git a/keys/cryptostore/holder.go b/keys/cryptostore/holder.go new file mode 100644 index 000000000..42d4662de --- /dev/null +++ b/keys/cryptostore/holder.go @@ -0,0 +1,125 @@ +package cryptostore + +import keys "github.com/tendermint/go-crypto/keys" + +// Manager combines encyption and storage implementation to provide +// a full-featured key manager +type Manager struct { + es encryptedStorage +} + +func New(coder Encoder, store keys.Storage) Manager { + return Manager{ + es: encryptedStorage{ + coder: coder, + store: store, + }, + } +} + +// 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 +} + +// 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: +// +func (s Manager) Create(name, passphrase, algo string) (keys.Info, error) { + gen, err := getGenerator(algo) + if err != nil { + return keys.Info{}, err + } + key := gen.Generate() + err = s.es.Put(name, passphrase, key) + return info(name, key), err +} + +// List loads the keys from the storage and enforces alphabetical order +func (s Manager) List() (keys.Infos, error) { + res, err := s.es.List() + res.Sort() + return res, err +} + +// Get returns the public information about one key +func (s Manager) Get(name string) (keys.Info, error) { + _, info, err := s.es.store.Get(name) + return info, err +} + +// Sign will modify the Signable in order to attach a valid signature with +// this public key +// +// If no key for this name, or the passphrase doesn't match, returns an error +func (s Manager) Sign(name, passphrase string, tx keys.Signable) error { + key, _, err := s.es.Get(name, passphrase) + if err != nil { + return err + } + sig := key.Sign(tx.SignBytes()) + pubkey := key.PubKey() + return tx.Sign(pubkey, sig) +} + +// Export decodes the private key with the current password, encodes +// it with a secure one-time password and generates a sequence that can be +// Imported by another Manager +// +// This is designed to copy from one device to another, or provide backups +// during version updates. +func (s Manager) Export(name, oldpass, transferpass string) ([]byte, error) { + key, _, err := s.es.Get(name, oldpass) + if err != nil { + return nil, err + } + + res, err := s.es.coder.Encrypt(key, transferpass) + return res, err +} + +// Import accepts bytes generated by Export along with the same transferpass +// If they are valid, it stores the password under the given name with the +// new passphrase. +func (s Manager) Import(name, newpass, transferpass string, data []byte) error { + key, err := s.es.coder.Decrypt(data, transferpass) + if err != nil { + return err + } + + return s.es.Put(name, newpass, key) +} + +// Delete removes key forever, but we must present the +// proper passphrase before deleting it (for security) +func (s Manager) Delete(name, passphrase string) error { + // verify we have the proper password before deleting + _, _, err := s.es.Get(name, passphrase) + if err != nil { + return err + } + return s.es.Delete(name) +} + +// Update changes the passphrase with which a already stored key is encoded. +// +// oldpass must be the current passphrase used for encoding, newpass will be +// the only valid passphrase from this time forward +func (s Manager) Update(name, oldpass, newpass string) error { + key, _, err := s.es.Get(name, oldpass) + if err != nil { + return err + } + + // we must delete first, as Putting over an existing name returns an error + s.Delete(name, oldpass) + + return s.es.Put(name, newpass, key) +} diff --git a/keys/cryptostore/holder_test.go b/keys/cryptostore/holder_test.go new file mode 100644 index 000000000..4f0383198 --- /dev/null +++ b/keys/cryptostore/holder_test.go @@ -0,0 +1,243 @@ +package cryptostore_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/go-crypto/keys/cryptostore" + "github.com/tendermint/go-crypto/keys/storage/memstorage" +) + +// TestKeyManagement makes sure we can manipulate these keys well +func TestKeyManagement(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + // make the storage with reasonable defaults + cstore := cryptostore.New( + cryptostore.SecretBox, + memstorage.New(), + ) + + algo := crypto.NameEd25519 + n1, n2, n3 := "personal", "business", "other" + p1, p2 := "1234", "really-secure!@#$" + + // Check empty state + l, err := cstore.List() + require.Nil(err) + assert.Empty(l) + + // create some keys + _, err = cstore.Get(n1) + assert.NotNil(err) + i, err := cstore.Create(n1, p1, algo) + require.Equal(n1, i.Name) + require.Nil(err) + _, err = cstore.Create(n2, p2, algo) + require.Nil(err) + + // we can get these keys + i2, err := cstore.Get(n2) + assert.Nil(err) + _, err = cstore.Get(n3) + assert.NotNil(err) + + // list shows them in order + keys, err := cstore.List() + require.Nil(err) + 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) + + // 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() + require.Nil(err) + assert.Equal(1, len(keys)) + _, err = cstore.Get(n1) + assert.NotNil(err) + + // make sure that it only signs with the right password + // tx := mock.NewSig([]byte("mytransactiondata")) + // err = cstore.Sign(n2, p1, tx) + // assert.NotNil(err) + // err = cstore.Sign(n2, p2, tx) + // assert.Nil(err, "%+v", err) + // sigs, err := tx.Signers() + // assert.Nil(err, "%+v", err) + // if assert.Equal(1, len(sigs)) { + // assert.Equal(i2.PubKey, sigs[0]) + // } +} + +// 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 assertPassword(assert *assert.Assertions, cstore cryptostore.Manager, name, pass, badpass string) { + err := cstore.Update(name, badpass, pass) + assert.NotNil(err) + err = cstore.Update(name, pass, pass) + assert.Nil(err, "%+v", err) +} + +// TestAdvancedKeyManagement verifies update, import, export functionality +func TestAdvancedKeyManagement(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + // make the storage with reasonable defaults + cstore := cryptostore.New( + cryptostore.SecretBox, + memstorage.New(), + ) + + algo := crypto.NameSecp256k1 + n1, n2 := "old-name", "new name" + p1, p2, p3, pt := "1234", "foobar", "ding booms!", "really-secure!@#$" + + // make sure key works with initial password + _, err := cstore.Create(n1, p1, algo) + require.Nil(err, "%+v", err) + assertPassword(assert, cstore, n1, p1, p2) + + // update password requires the existing password + err = cstore.Update(n1, "jkkgkg", p2) + assert.NotNil(err) + assertPassword(assert, cstore, n1, p1, p2) + + // then it changes the password when correct + err = cstore.Update(n1, p1, p2) + assert.Nil(err) + // p2 is now the proper one! + assertPassword(assert, cstore, n1, p2, p1) + + // exporting requires the proper name and passphrase + _, err = cstore.Export(n2, p2, pt) + assert.NotNil(err) + _, err = cstore.Export(n1, p1, pt) + assert.NotNil(err) + 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) + assert.NotNil(err) + // import cannot overwrite existing keys + err = cstore.Import(n1, p3, pt, exported) + assert.NotNil(err) + // we can now import under another name + err = cstore.Import(n2, p3, pt, exported) + require.Nil(err, "%+v", err) + + // make sure both passwords are now properly set (not to the transfer pass) + assertPassword(assert, cstore, n1, p2, pt) + assertPassword(assert, cstore, n2, p3, pt) +} + +// 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 +// } diff --git a/keys/cryptostore/storage_test.go b/keys/cryptostore/storage_test.go new file mode 100644 index 000000000..b109c44e8 --- /dev/null +++ b/keys/cryptostore/storage_test.go @@ -0,0 +1,41 @@ +package cryptostore + +import ( + "testing" + + "github.com/stretchr/testify/assert" + keys "github.com/tendermint/go-crypto/keys" +) + +func TestSortKeys(t *testing.T) { + assert := assert.New(t) + + gen := GenEd25519.Generate + assert.NotEqual(gen(), gen()) + + // alphabetical order is n3, n1, n2 + n1, n2, n3 := "john", "mike", "alice" + infos := keys.Infos{ + info(n1, gen()), + info(n2, gen()), + info(n3, gen()), + } + + // make sure they are initialized unsorted + assert.Equal(n1, infos[0].Name) + assert.Equal(n2, infos[1].Name) + assert.Equal(n3, infos[2].Name) + + // now they are sorted + infos.Sort() + assert.Equal(n3, infos[0].Name) + assert.Equal(n1, infos[1].Name) + assert.Equal(n2, infos[2].Name) + + // make sure info put some real data there... + assert.NotEmpty(infos[0].PubKey) + assert.NotEmpty(infos[0].PubKey.Address()) + assert.NotEmpty(infos[1].PubKey) + assert.NotEmpty(infos[1].PubKey.Address()) + assert.NotEqual(infos[0].PubKey, infos[1].PubKey) +} diff --git a/keys/keys.toml b/keys/keys.toml new file mode 100644 index 000000000..f9eb95e1c --- /dev/null +++ b/keys/keys.toml @@ -0,0 +1,2 @@ +output = "text" +keydir = ".mykeys" diff --git a/keys/server/README.md b/keys/server/README.md new file mode 100644 index 000000000..032cf574e --- /dev/null +++ b/keys/server/README.md @@ -0,0 +1,13 @@ +# 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 new file mode 100644 index 000000000..710e4f392 --- /dev/null +++ b/keys/server/helpers.go @@ -0,0 +1,58 @@ +/* +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 new file mode 100644 index 000000000..90d6da2b0 --- /dev/null +++ b/keys/server/keys.go @@ -0,0 +1,127 @@ +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, err := k.manager.Create(req.Name, req.Passphrase, req.Algo) + if err != nil { + writeError(w, err) + return + } + + writeSuccess(w, &key) +} + +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 new file mode 100644 index 000000000..4908559b0 --- /dev/null +++ b/keys/server/keys_test.go @@ -0,0 +1,190 @@ +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(key.Name, n1) + + // 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.Name, n2) + + // 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(k.Name, n1) + assert.NotNil(k.Address) + assert.Equal(k.Address, key.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(), + ) + + // 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) (*keys.Info, 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 := keys.Info{} + 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 new file mode 100644 index 000000000..ffdc542f1 --- /dev/null +++ b/keys/server/types/keys.go @@ -0,0 +1,28 @@ +package types + +// 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 +} diff --git a/keys/server/valid.go b/keys/server/valid.go new file mode 100644 index 000000000..50b51e21b --- /dev/null +++ b/keys/server/valid.go @@ -0,0 +1,12 @@ +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 new file mode 100644 index 000000000..0c25eb8a5 --- /dev/null +++ b/keys/storage.go @@ -0,0 +1,10 @@ +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 new file mode 100644 index 000000000..696b200fc --- /dev/null +++ b/keys/storage/filestorage/main.go @@ -0,0 +1,171 @@ +/* +package filestorage provides a secure on-disk storage of private keys and +metadata. Security is enforced by file and directory permissions, much +like standard ssh key storage. +*/ +package filestorage + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "strings" + + "github.com/pkg/errors" + crypto "github.com/tendermint/go-crypto" + keys "github.com/tendermint/go-crypto/keys" +) + +const ( + BlockType = "Tendermint Light Client" + PrivExt = "tlc" + PubExt = "pub" + keyPerm = os.FileMode(0600) + pubPerm = os.FileMode(0644) + dirPerm = os.FileMode(0700) +) + +type FileStore struct { + keyDir string +} + +// New creates an instance of file-based key storage with tight permissions +// +// dir should be an absolute path of a directory owner by this user. It will +// be created if it doesn't exist already. +func New(dir string) FileStore { + err := os.MkdirAll(dir, dirPerm) + if err != nil { + panic(err) + } + return FileStore{dir} +} + +// assertStorage just makes sure we implement the proper Storage interface +func (s FileStore) assertStorage() keys.Storage { + return s +} + +// Put creates two files, one with the public info as json, the other +// with the (encoded) private key as gpg ascii-armor style +func (s FileStore) Put(name string, key []byte, info keys.Info) error { + pub, priv := s.nameToPaths(name) + + // write public info + err := writeInfo(pub, info) + if err != nil { + return err + } + + // write private info + return write(priv, name, key) +} + +// Get loads the info and (encoded) private key from the directory +// It uses `name` to generate the filename, and returns an error if the +// files don't exist or are in the incorrect format +func (s FileStore) Get(name string) ([]byte, keys.Info, error) { + pub, priv := s.nameToPaths(name) + + info, err := readInfo(pub) + if err != nil { + return nil, info, err + } + + key, _, err := read(priv) + return key, info.Format(), err +} + +// List parses the key directory for public info and returns a list of +// Info for all keys located in this directory. +func (s FileStore) List() (keys.Infos, error) { + dir, err := os.Open(s.keyDir) + if err != nil { + return nil, errors.Wrap(err, "List Keys") + } + names, err := dir.Readdirnames(0) + if err != nil { + return nil, errors.Wrap(err, "List Keys") + } + + // filter names for .pub ending and load them one by one + // half the files is a good guess for pre-allocating the slice + infos := make([]keys.Info, 0, len(names)/2) + for _, name := range names { + if strings.HasSuffix(name, PubExt) { + p := path.Join(s.keyDir, name) + info, err := readInfo(p) + if err != nil { + return nil, err + } + infos = append(infos, info.Format()) + } + } + + return infos, nil +} + +// Delete permanently removes the public and private info for the named key +// The calling function should provide some security checks first. +func (s FileStore) Delete(name string) error { + pub, priv := s.nameToPaths(name) + err := os.Remove(priv) + if err != nil { + return errors.Wrap(err, "Deleting Private Key") + } + err = os.Remove(pub) + return errors.Wrap(err, "Deleting Public Key") +} + +func (s FileStore) nameToPaths(name string) (pub, priv string) { + privName := fmt.Sprintf("%s.%s", name, PrivExt) + pubName := fmt.Sprintf("%s.%s", name, PubExt) + return path.Join(s.keyDir, pubName), path.Join(s.keyDir, privName) +} + +func writeInfo(path string, info keys.Info) error { + return write(path, info.Name, info.PubKey.Bytes()) +} + +func readInfo(path string) (info keys.Info, err error) { + var data []byte + data, info.Name, err = read(path) + if err != nil { + return + } + pk, err := crypto.PubKeyFromBytes(data) + info.PubKey = pk + return +} + +func read(path string) ([]byte, string, error) { + f, err := os.Open(path) + if err != nil { + return nil, "", errors.Wrap(err, "Reading data") + } + d, err := ioutil.ReadAll(f) + if err != nil { + return nil, "", errors.Wrap(err, "Reading data") + } + block, headers, key, err := crypto.DecodeArmor(string(d)) + if err != nil { + return nil, "", errors.Wrap(err, "Invalid Armor") + } + if block != BlockType { + return nil, "", errors.Errorf("Unknown key type: %s", block) + } + return key, headers["name"], nil +} + +func write(path, name string, key []byte) error { + f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, keyPerm) + if err != nil { + return errors.Wrap(err, "Writing data") + } + defer f.Close() + headers := map[string]string{"name": name} + text := crypto.EncodeArmor(BlockType, headers, key) + _, err = f.WriteString(text) + return errors.Wrap(err, "Writing data") +} diff --git a/keys/storage/filestorage/main_test.go b/keys/storage/filestorage/main_test.go new file mode 100644 index 000000000..28c950c2c --- /dev/null +++ b/keys/storage/filestorage/main_test.go @@ -0,0 +1,106 @@ +package filestorage + +import ( + "io/ioutil" + "os" + "path" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + crypto "github.com/tendermint/go-crypto" + keys "github.com/tendermint/go-crypto/keys" +) + +func TestBasicCRUD(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + dir, err := ioutil.TempDir("", "filestorage-test") + assert.Nil(err) + defer os.RemoveAll(dir) + store := New(dir) + + name := "bar" + key := []byte("secret-key-here") + pubkey := crypto.GenPrivKeyEd25519().PubKey() + info := keys.Info{ + Name: name, + PubKey: pubkey.Wrap(), + } + + // No data: Get and Delete return nothing + _, _, err = store.Get(name) + assert.NotNil(err) + err = store.Delete(name) + assert.NotNil(err) + // List returns empty list + l, err := store.List() + assert.Nil(err) + assert.Empty(l) + + // Putting the key in the store must work + err = store.Put(name, key, info) + assert.Nil(err) + // But a second time is a failure + err = store.Put(name, key, info) + assert.NotNil(err) + + // Now, we can get and list properly + k, i, err := store.Get(name) + require.Nil(err, "%+v", err) + assert.Equal(key, k) + assert.Equal(info.Name, i.Name) + assert.Equal(info.PubKey, i.PubKey) + assert.NotEmpty(i.Address) + l, err = store.List() + require.Nil(err, "%+v", err) + assert.Equal(1, len(l)) + assert.Equal(i, l[0]) + + // querying a non-existent key fails + _, _, err = store.Get("badname") + assert.NotNil(err) + + // We can only delete once + err = store.Delete(name) + assert.Nil(err) + err = store.Delete(name) + assert.NotNil(err) + + // and then Get and List don't work + _, _, err = store.Get(name) + assert.NotNil(err) + // List returns empty list + l, err = store.List() + assert.Nil(err) + assert.Empty(l) +} + +func TestDirectoryHandling(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + // prepare a temp dir and make sure it is not there + newDir := path.Join(os.TempDir(), "file-test-dir") + _, err := os.Open(newDir) + assert.True(os.IsNotExist(err)) + defer os.RemoveAll(newDir) + + // now, check with two levels deep.... + parentDir := path.Join(os.TempDir(), "missing-dir") + nestedDir := path.Join(parentDir, "lots", "of", "levels", "here") + _, err = os.Open(parentDir) + assert.True(os.IsNotExist(err)) + defer os.RemoveAll(parentDir) + + // create a new storage, and verify it creates the directory with good permissions + for _, dir := range []string{newDir, nestedDir, newDir} { + New(dir) + d, err := os.Open(dir) + require.Nil(err) + defer d.Close() + + stat, err := d.Stat() + require.Nil(err) + assert.Equal(dirPerm, stat.Mode()&os.ModePerm) + } +} diff --git a/keys/storage/memstorage/main.go b/keys/storage/memstorage/main.go new file mode 100644 index 000000000..195fa7a17 --- /dev/null +++ b/keys/storage/memstorage/main.go @@ -0,0 +1,70 @@ +/* +package memstorage provides a simple in-memory key store designed for +use in test cases, particularly to isolate them from the filesystem, +concurrency, and cleanup issues. +*/ +package memstorage + +import ( + "github.com/pkg/errors" + keys "github.com/tendermint/go-crypto/keys" +) + +type data struct { + info keys.Info + key []byte +} + +type MemStore map[string]data + +// New creates an instance of file-based key storage with tight permissions +func New() MemStore { + return MemStore{} +} + +// assertStorage just makes sure we implement the Storage interface +func (s MemStore) assertStorage() keys.Storage { + return s +} + +// Put adds the given key, returns an error if it another key +// is already stored under this name +func (s MemStore) Put(name string, key []byte, info keys.Info) error { + if _, ok := s[name]; ok { + return errors.Errorf("Key named '%s' already exists", name) + } + s[name] = data{info, key} + return nil +} + +// Get returns the key stored under the name, or returns an error if not present +func (s MemStore) Get(name string) ([]byte, keys.Info, error) { + var err error + d, ok := s[name] + if !ok { + err = errors.Errorf("Key named '%s' doesn't exist", name) + } + return d.key, d.info.Format(), err +} + +// List returns the public info of all keys in the MemStore in unsorted order +func (s MemStore) List() (keys.Infos, error) { + res := make([]keys.Info, len(s)) + i := 0 + for _, d := range s { + res[i] = d.info.Format() + i++ + } + return res, nil +} + +// Delete removes the named key from the MemStore, raising an error if it +// wasn't present yet. +func (s MemStore) Delete(name string) error { + _, ok := s[name] + if !ok { + return errors.Errorf("Key named '%s' doesn't exist", name) + } + delete(s, name) + return nil +} diff --git a/keys/storage/memstorage/main_test.go b/keys/storage/memstorage/main_test.go new file mode 100644 index 000000000..feccb387f --- /dev/null +++ b/keys/storage/memstorage/main_test.go @@ -0,0 +1,69 @@ +package memstorage + +import ( + "testing" + + "github.com/stretchr/testify/assert" + crypto "github.com/tendermint/go-crypto" + keys "github.com/tendermint/go-crypto/keys" +) + +func TestBasicCRUD(t *testing.T) { + assert := assert.New(t) + store := New() + + name := "foo" + key := []byte("secret-key-here") + pubkey := crypto.GenPrivKeyEd25519().PubKey() + info := keys.Info{ + Name: name, + PubKey: pubkey, + } + + // No data: Get and Delete return nothing + _, _, err := store.Get(name) + assert.NotNil(err) + err = store.Delete(name) + assert.NotNil(err) + // List returns empty list + l, err := store.List() + assert.Nil(err) + assert.Empty(l) + + // Putting the key in the store must work + err = store.Put(name, key, info) + assert.Nil(err) + // But a second time is a failure + err = store.Put(name, key, info) + assert.NotNil(err) + + // Now, we can get and list properly + k, i, err := store.Get(name) + assert.Nil(err) + assert.Equal(key, k) + assert.Equal(info.Name, i.Name) + assert.Equal(info.PubKey, i.PubKey) + assert.NotEmpty(i.Address) + l, err = store.List() + assert.Nil(err) + assert.Equal(1, len(l)) + assert.Equal(i, l[0]) + + // querying a non-existent key fails + _, _, err = store.Get("badname") + assert.NotNil(err) + + // We can only delete once + err = store.Delete(name) + assert.Nil(err) + err = store.Delete(name) + assert.NotNil(err) + + // and then Get and List don't work + _, _, err = store.Get(name) + assert.NotNil(err) + // List returns empty list + l, err = store.List() + assert.Nil(err) + assert.Empty(l) +} diff --git a/keys/transactions.go b/keys/transactions.go new file mode 100644 index 000000000..aafb77873 --- /dev/null +++ b/keys/transactions.go @@ -0,0 +1,70 @@ +package keys + +import ( + "sort" + + crypto "github.com/tendermint/go-crypto" + data "github.com/tendermint/go-wire/data" +) + +// Info is the public information about a key +type Info struct { + Name string `json:"name"` + Address data.Bytes `json:"address"` + PubKey crypto.PubKey `json:"pubkey"` +} + +func (i *Info) Format() Info { + if !i.PubKey.Empty() { + i.Address = i.PubKey.Address() + } + return *i +} + +// Infos is a wrapper to allows alphabetical sorting of the keys +type Infos []Info + +func (k Infos) Len() int { return len(k) } +func (k Infos) Less(i, j int) bool { return k[i].Name < k[j].Name } +func (k Infos) Swap(i, j int) { k[i], k[j] = k[j], k[i] } +func (k Infos) Sort() { + if k != nil { + sort.Sort(k) + } +} + +// Signable represents any transaction we wish to send to tendermint core +// These methods allow us to sign arbitrary Tx with the KeyStore +type Signable interface { + // SignBytes is the immutable data, which needs to be signed + SignBytes() []byte + + // Sign will add a signature and pubkey. + // + // Depending on the Signable, one may be able to call this multiple times for multisig + // Returns error if called with invalid data or too many times + Sign(pubkey crypto.PubKey, sig crypto.Signature) error + + // Signers will return the public key(s) that signed if the signature + // is valid, or an error if there is any issue with the signature, + // including if there are no signatures + Signers() ([]crypto.PubKey, error) + + // TxBytes returns the transaction data as well as all signatures + // It should return an error if Sign was never called + TxBytes() ([]byte, error) +} + +// Signer allows one to use a keystore to sign transactions +type Signer interface { + Sign(name, passphrase string, tx Signable) error +} + +// Manager allows simple CRUD on a keystore, as an aid to signing +type Manager interface { + Create(name, passphrase, algo string) (Info, error) + List() (Infos, error) + Get(name string) (Info, error) + Update(name, oldpass, newpass string) error + Delete(name, passphrase string) error +} diff --git a/keys/tx/docs.go b/keys/tx/docs.go new file mode 100644 index 000000000..6a5ea3ce3 --- /dev/null +++ b/keys/tx/docs.go @@ -0,0 +1,10 @@ +/* +package tx contains generic Signable implementations that can be used +by your application or tests to handle authentication needs. + +It currently supports transaction data as opaque bytes and either single +or multiple private key signatures using straightforward algorithms. +It currently does not support N-of-M key share signing of other more +complex algorithms (although it would be great to add them) +*/ +package tx diff --git a/keys/tx/multi.go b/keys/tx/multi.go new file mode 100644 index 000000000..f069fb273 --- /dev/null +++ b/keys/tx/multi.go @@ -0,0 +1,67 @@ +package tx + +import ( + "github.com/pkg/errors" + crypto "github.com/tendermint/go-crypto" + data "github.com/tendermint/go-wire/data" +) + +// MultiSig lets us wrap arbitrary data with a go-crypto signature +// +// TODO: rethink how we want to integrate this with KeyStore so it makes +// more sense (particularly the verify method) +type MultiSig struct { + Data data.Bytes + Sigs []Signed +} + +type Signed struct { + Sig crypto.Signature + Pubkey crypto.PubKey +} + +var _ SigInner = &MultiSig{} + +func NewMulti(data []byte) Sig { + return Sig{&MultiSig{Data: data}} +} + +// SignBytes returns the original data passed into `NewSig` +func (s *MultiSig) 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 *MultiSig) Sign(pubkey crypto.PubKey, sig crypto.Signature) error { + if pubkey.Empty() || sig.Empty() { + return errors.New("Signature or Key missing") + } + + // set the value once we are happy + x := Signed{sig, pubkey} + s.Sigs = append(s.Sigs, x) + 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 *MultiSig) Signers() ([]crypto.PubKey, error) { + if len(s.Sigs) == 0 { + return nil, errors.New("Never signed") + } + + keys := make([]crypto.PubKey, len(s.Sigs)) + for i := range s.Sigs { + ms := s.Sigs[i] + if !ms.Pubkey.VerifyBytes(s.Data, ms.Sig) { + return nil, errors.Errorf("Signature %d doesn't match (key: %X)", i, ms.Pubkey.Bytes()) + } + keys[i] = ms.Pubkey + } + + return keys, nil +} diff --git a/keys/tx/multi_test.go b/keys/tx/multi_test.go new file mode 100644 index 000000000..97463a5f7 --- /dev/null +++ b/keys/tx/multi_test.go @@ -0,0 +1,77 @@ +package tx + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + crypto "github.com/tendermint/go-crypto" + keys "github.com/tendermint/go-crypto/keys" + "github.com/tendermint/go-crypto/keys/cryptostore" + "github.com/tendermint/go-crypto/keys/storage/memstorage" +) + +func TestMultiSig(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + algo := crypto.NameEd25519 + cstore := cryptostore.New( + cryptostore.SecretBox, + memstorage.New(), + ) + n, p := "foo", "bar" + n2, p2 := "other", "thing" + + acct, err := cstore.Create(n, p, algo) + require.Nil(err, "%+v", err) + acct2, err := cstore.Create(n2, p2, algo) + require.Nil(err, "%+v", err) + + type signer struct { + key keys.Info + name, pass string + } + cases := []struct { + data string + signers []signer + }{ + {"one", []signer{{acct, n, p}}}, + {"two", []signer{{acct2, n2, p2}}}, + {"both", []signer{{acct, n, p}, {acct2, n2, p2}}}, + } + + for _, tc := range cases { + tx := NewMulti([]byte(tc.data)) + // unsigned version + _, err = tx.Signers() + assert.NotNil(err) + orig, err := tx.TxBytes() + require.Nil(err, "%+v", err) + data := tx.SignBytes() + assert.Equal(tc.data, string(data)) + + // sign it + for _, s := range tc.signers { + err = cstore.Sign(s.name, s.pass, tx) + require.Nil(err, "%+v", err) + } + + // make sure it is proper now + sigs, err := tx.Signers() + require.Nil(err, "%+v", err) + if assert.Equal(len(tc.signers), len(sigs)) { + for i := range sigs { + // This must be refactored... + assert.Equal(tc.signers[i].key.PubKey, sigs[i]) + } + } + // the tx bytes should change after this + after, err := tx.TxBytes() + require.Nil(err, "%+v", err) + assert.NotEqual(orig, after, "%X != %X", orig, after) + + // sign bytes are the same + data = tx.SignBytes() + assert.Equal(tc.data, string(data)) + } +} diff --git a/keys/tx/one.go b/keys/tx/one.go new file mode 100644 index 000000000..af468cc2c --- /dev/null +++ b/keys/tx/one.go @@ -0,0 +1,57 @@ +package tx + +import ( + "github.com/pkg/errors" + crypto "github.com/tendermint/go-crypto" + data "github.com/tendermint/go-wire/data" +) + +// OneSig lets us wrap arbitrary data with a go-crypto signature +// +// TODO: rethink how we want to integrate this with KeyStore so it makes +// more sense (particularly the verify method) +type OneSig struct { + Data data.Bytes + Signed +} + +var _ SigInner = &OneSig{} + +func New(data []byte) Sig { + return WrapSig(&OneSig{Data: data}) +} + +// SignBytes returns the original data passed into `NewSig` +func (s *OneSig) 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 *OneSig) Sign(pubkey crypto.PubKey, sig crypto.Signature) error { + if pubkey.Empty() || sig.Empty() { + return errors.New("Signature or Key missing") + } + if !s.Sig.Empty() { + return errors.New("Transaction can only be signed once") + } + + // set the value once we are happy + s.Signed = Signed{sig, pubkey} + 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 *OneSig) Signers() ([]crypto.PubKey, error) { + if s.Pubkey.Empty() || s.Sig.Empty() { + return nil, errors.New("Never signed") + } + if !s.Pubkey.VerifyBytes(s.Data, s.Sig) { + return nil, errors.New("Signature doesn't match") + } + return []crypto.PubKey{s.Pubkey}, nil +} diff --git a/keys/tx/one_test.go b/keys/tx/one_test.go new file mode 100644 index 000000000..05af347b4 --- /dev/null +++ b/keys/tx/one_test.go @@ -0,0 +1,73 @@ +package tx + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + crypto "github.com/tendermint/go-crypto" + keys "github.com/tendermint/go-crypto/keys" + "github.com/tendermint/go-crypto/keys/cryptostore" + "github.com/tendermint/go-crypto/keys/storage/memstorage" +) + +func TestOneSig(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + algo := crypto.NameEd25519 + cstore := cryptostore.New( + cryptostore.SecretBox, + memstorage.New(), + ) + n, p := "foo", "bar" + n2, p2 := "other", "thing" + + acct, err := cstore.Create(n, p, algo) + require.Nil(err, "%+v", err) + acct2, err := cstore.Create(n2, p2, algo) + require.Nil(err, "%+v", err) + + cases := []struct { + data string + key keys.Info + name, pass string + }{ + {"first", acct, n, p}, + {"kehfkhefy8y", acct, n, p}, + {"second", acct2, n2, p2}, + } + + for _, tc := range cases { + tx := New([]byte(tc.data)) + // unsigned version + _, err = tx.Signers() + assert.NotNil(err) + orig, err := tx.TxBytes() + require.Nil(err, "%+v", err) + data := tx.SignBytes() + assert.Equal(tc.data, string(data)) + + // sign it + err = cstore.Sign(tc.name, tc.pass, tx) + require.Nil(err, "%+v", err) + // but not twice + err = cstore.Sign(tc.name, tc.pass, tx) + require.NotNil(err) + + // make sure it is proper now + sigs, err := tx.Signers() + require.Nil(err, "%+v", err) + if assert.Equal(1, len(sigs)) { + // This must be refactored... + assert.Equal(tc.key.PubKey, sigs[0]) + } + // the tx bytes should change after this + after, err := tx.TxBytes() + require.Nil(err, "%+v", err) + assert.NotEqual(orig, after, "%X != %X", orig, after) + + // sign bytes are the same + data = tx.SignBytes() + assert.Equal(tc.data, string(data)) + } +} diff --git a/keys/tx/reader.go b/keys/tx/reader.go new file mode 100644 index 000000000..265e88b46 --- /dev/null +++ b/keys/tx/reader.go @@ -0,0 +1,76 @@ +package tx + +import ( + crypto "github.com/tendermint/go-crypto" + keys "github.com/tendermint/go-crypto/keys" + data "github.com/tendermint/go-wire/data" +) + +const ( + typeOneSig = byte(0x01) + typeMultiSig = byte(0x02) + nameOneSig = "sig" + nameMultiSig = "multi" +) + +var _ keys.Signable = Sig{} +var TxMapper data.Mapper + +func init() { + TxMapper = data.NewMapper(Sig{}). + RegisterImplementation(&OneSig{}, nameOneSig, typeOneSig). + RegisterImplementation(&MultiSig{}, nameMultiSig, typeMultiSig) +} + +/* +DO NOT USE this interface. + +It is public by necessity but should never be used directly +outside of this package. + +Only use Sig, never SigInner +*/ +type SigInner interface { + SignBytes() []byte + Sign(pubkey crypto.PubKey, sig crypto.Signature) error + Signers() ([]crypto.PubKey, error) +} + +// Sig is what is exported, and handles serialization +type Sig struct { + SigInner +} + +// TxBytes +func (s Sig) TxBytes() ([]byte, error) { + return data.ToWire(s) +} + +// WrapSig goes from concrete implementation to "interface" struct +func WrapSig(pk SigInner) Sig { + if wrap, ok := pk.(Sig); ok { + pk = wrap.Unwrap() + } + return Sig{pk} +} + +// Unwrap recovers the concrete interface safely (regardless of levels of embeds) +func (p Sig) Unwrap() SigInner { + pk := p.SigInner + for wrap, ok := pk.(Sig); ok; wrap, ok = pk.(Sig) { + pk = wrap.SigInner + } + return pk +} + +func (p Sig) MarshalJSON() ([]byte, error) { + return TxMapper.ToJSON(p.Unwrap()) +} + +func (p *Sig) UnmarshalJSON(data []byte) (err error) { + parsed, err := TxMapper.FromJSON(data) + if err == nil && parsed != nil { + p.SigInner = parsed.(SigInner) + } + return +} diff --git a/keys/tx/reader_test.go b/keys/tx/reader_test.go new file mode 100644 index 000000000..c50481f14 --- /dev/null +++ b/keys/tx/reader_test.go @@ -0,0 +1,70 @@ +package tx + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + crypto "github.com/tendermint/go-crypto" + data "github.com/tendermint/go-wire/data" + "github.com/tendermint/go-crypto/keys/cryptostore" + "github.com/tendermint/go-crypto/keys/storage/memstorage" +) + +func TestReader(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + algo := crypto.NameEd25519 + cstore := cryptostore.New( + cryptostore.SecretBox, + memstorage.New(), + ) + type sigs struct{ name, pass string } + u := sigs{"alice", "1234"} + u2 := sigs{"bob", "foobar"} + + _, err := cstore.Create(u.name, u.pass, algo) + require.Nil(err, "%+v", err) + _, err = cstore.Create(u2.name, u2.pass, algo) + require.Nil(err, "%+v", err) + + cases := []struct { + tx Sig + sigs []sigs + }{ + {New([]byte("first")), nil}, + {New([]byte("second")), []sigs{u}}, + {New([]byte("other")), []sigs{u2}}, + {NewMulti([]byte("m-first")), nil}, + {NewMulti([]byte("m-second")), []sigs{u}}, + {NewMulti([]byte("m-other")), []sigs{u, u2}}, + } + + for _, tc := range cases { + tx := tc.tx + + // make sure json serialization and loading works w/o sigs + var pre Sig + pjs, err := data.ToJSON(tx) + require.Nil(err, "%+v", err) + err = data.FromJSON(pjs, &pre) + require.Nil(err, "%+v", err) + assert.Equal(tx, pre) + + for _, s := range tc.sigs { + err = cstore.Sign(s.name, s.pass, tx) + require.Nil(err, "%+v", err) + } + + var post Sig + sjs, err := data.ToJSON(tx) + require.Nil(err, "%+v", err) + err = data.FromJSON(sjs, &post) + require.Nil(err, "%+v\n%s", err, string(sjs)) + assert.Equal(tx, post) + + if len(tc.sigs) > 0 { + assert.NotEqual(pjs, sjs, "%s\n ------ %s", string(pjs), string(sjs)) + } + } +} diff --git a/priv_key.go b/priv_key.go index 3a9a5f69b..85f8a8ec4 100644 --- a/priv_key.go +++ b/priv_key.go @@ -6,91 +6,89 @@ import ( secp256k1 "github.com/btcsuite/btcd/btcec" "github.com/tendermint/ed25519" "github.com/tendermint/ed25519/extra25519" - . "github.com/tendermint/go-common" - data "github.com/tendermint/go-data" + . "github.com/tendermint/tmlibs/common" + data "github.com/tendermint/go-wire/data" "github.com/tendermint/go-wire" ) -// PrivKey is part of PrivAccount and state.PrivValidator. -type PrivKey interface { - Bytes() []byte - Sign(msg []byte) Signature - PubKey() PubKey - Equals(PrivKey) bool +func PrivKeyFromBytes(privKeyBytes []byte) (privKey PrivKey, err error) { + err = wire.ReadBinaryBytes(privKeyBytes, &privKey) + return } -// Types of implementations -const ( - TypeEd25519 = byte(0x01) - TypeSecp256k1 = byte(0x02) - NameEd25519 = "ed25519" - NameSecp256k1 = "secp256k1" -) +//---------------------------------------- -var privKeyMapper data.Mapper - -// register both private key types with go-data (and thus go-wire) -func init() { - privKeyMapper = data.NewMapper(PrivKeyS{}). - RegisterImplementation(PrivKeyEd25519{}, NameEd25519, TypeEd25519). - RegisterImplementation(PrivKeySecp256k1{}, NameSecp256k1, TypeSecp256k1) -} - -// PrivKeyS add json serialization to PrivKey -type PrivKeyS struct { - PrivKey +type PrivKey struct { + PrivKeyInner `json:"unwrap"` } -func WrapPrivKey(pk PrivKey) PrivKeyS { - for ppk, ok := pk.(PrivKeyS); ok; ppk, ok = pk.(PrivKeyS) { - pk = ppk.PrivKey - } - return PrivKeyS{pk} +// DO NOT USE THIS INTERFACE. +// You probably want to use PubKey +type PrivKeyInner interface { + AssertIsPrivKeyInner() + Bytes() []byte + Sign(msg []byte) Signature + PubKey() PubKey + Equals(PrivKey) bool + Wrap() PrivKey } -func (p PrivKeyS) MarshalJSON() ([]byte, error) { - return privKeyMapper.ToJSON(p.PrivKey) +func (p PrivKey) MarshalJSON() ([]byte, error) { + return privKeyMapper.ToJSON(p.PrivKeyInner) } -func (p *PrivKeyS) UnmarshalJSON(data []byte) (err error) { +func (p *PrivKey) UnmarshalJSON(data []byte) (err error) { parsed, err := privKeyMapper.FromJSON(data) if err == nil && parsed != nil { - p.PrivKey = parsed.(PrivKey) + p.PrivKeyInner = parsed.(PrivKeyInner) } return } -func (p PrivKeyS) Empty() bool { - return p.PrivKey == nil +// Unwrap recovers the concrete interface safely (regardless of levels of embeds) +func (p PrivKey) Unwrap() PrivKeyInner { + pk := p.PrivKeyInner + for wrap, ok := pk.(PrivKey); ok; wrap, ok = pk.(PrivKey) { + pk = wrap.PrivKeyInner + } + return pk } -func PrivKeyFromBytes(privKeyBytes []byte) (privKey PrivKey, err error) { - err = wire.ReadBinaryBytes(privKeyBytes, &privKey) - return +func (p PrivKey) Empty() bool { + return p.PrivKeyInner == nil } +var privKeyMapper = data.NewMapper(PrivKey{}). + RegisterImplementation(PrivKeyEd25519{}, NameEd25519, TypeEd25519). + RegisterImplementation(PrivKeySecp256k1{}, NameSecp256k1, TypeSecp256k1) + //------------------------------------- +var _ PrivKeyInner = PrivKeyEd25519{} + // Implements PrivKey type PrivKeyEd25519 [64]byte +func (privKey PrivKeyEd25519) AssertIsPrivKeyInner() {} + func (privKey PrivKeyEd25519) Bytes() []byte { - return wire.BinaryBytes(struct{ PrivKey }{privKey}) + return wire.BinaryBytes(PrivKey{privKey}) } func (privKey PrivKeyEd25519) Sign(msg []byte) Signature { privKeyBytes := [64]byte(privKey) signatureBytes := ed25519.Sign(&privKeyBytes, msg) - return SignatureEd25519(*signatureBytes) + return SignatureEd25519(*signatureBytes).Wrap() } func (privKey PrivKeyEd25519) PubKey() PubKey { privKeyBytes := [64]byte(privKey) - return PubKeyEd25519(*ed25519.MakePublicKey(&privKeyBytes)) + pubBytes := *ed25519.MakePublicKey(&privKeyBytes) + return PubKeyEd25519(pubBytes).Wrap() } func (privKey PrivKeyEd25519) Equals(other PrivKey) bool { - if otherEd, ok := other.(PrivKeyEd25519); ok { + if otherEd, ok := other.Unwrap().(PrivKeyEd25519); ok { return bytes.Equal(privKey[:], otherEd[:]) } else { return false @@ -130,6 +128,10 @@ func (privKey PrivKeyEd25519) Generate(index int) PrivKeyEd25519 { return PrivKeyEd25519(newKey) } +func (privKey PrivKeyEd25519) Wrap() PrivKey { + return PrivKey{privKey} +} + func GenPrivKeyEd25519() PrivKeyEd25519 { privKeyBytes := new([64]byte) copy(privKeyBytes[:32], CRandBytes(32)) @@ -149,11 +151,15 @@ func GenPrivKeyEd25519FromSecret(secret []byte) PrivKeyEd25519 { //------------------------------------- +var _ PrivKeyInner = PrivKeySecp256k1{} + // Implements PrivKey type PrivKeySecp256k1 [32]byte +func (privKey PrivKeySecp256k1) AssertIsPrivKeyInner() {} + func (privKey PrivKeySecp256k1) Bytes() []byte { - return wire.BinaryBytes(struct{ PrivKey }{privKey}) + return wire.BinaryBytes(PrivKey{privKey}) } func (privKey PrivKeySecp256k1) Sign(msg []byte) Signature { @@ -162,18 +168,18 @@ func (privKey PrivKeySecp256k1) Sign(msg []byte) Signature { if err != nil { PanicSanity(err) } - return SignatureSecp256k1(sig__.Serialize()) + return SignatureSecp256k1(sig__.Serialize()).Wrap() } func (privKey PrivKeySecp256k1) PubKey() PubKey { _, pub__ := secp256k1.PrivKeyFromBytes(secp256k1.S256(), privKey[:]) var pub PubKeySecp256k1 copy(pub[:], pub__.SerializeCompressed()) - return pub + return pub.Wrap() } func (privKey PrivKeySecp256k1) Equals(other PrivKey) bool { - if otherSecp, ok := other.(PrivKeySecp256k1); ok { + if otherSecp, ok := other.Unwrap().(PrivKeySecp256k1); ok { return bytes.Equal(privKey[:], otherSecp[:]) } else { return false @@ -195,6 +201,10 @@ func (privKey PrivKeySecp256k1) String() string { return Fmt("PrivKeySecp256k1{*****}") } +func (privKey PrivKeySecp256k1) Wrap() PrivKey { + return PrivKey{privKey} +} + /* // Deterministically generates new priv-key bytes from key. func (key PrivKeySecp256k1) Generate(index int) PrivKeySecp256k1 { diff --git a/pub_key.go b/pub_key.go index a047edb2f..99839f288 100644 --- a/pub_key.go +++ b/pub_key.go @@ -7,68 +7,73 @@ import ( secp256k1 "github.com/btcsuite/btcd/btcec" "github.com/tendermint/ed25519" "github.com/tendermint/ed25519/extra25519" - . "github.com/tendermint/go-common" - data "github.com/tendermint/go-data" + . "github.com/tendermint/tmlibs/common" + data "github.com/tendermint/go-wire/data" "github.com/tendermint/go-wire" "golang.org/x/crypto/ripemd160" ) -// PubKey is part of Account and Validator. -type PubKey interface { - Address() []byte - Bytes() []byte - KeyString() string - VerifyBytes(msg []byte, sig Signature) bool - Equals(PubKey) bool +func PubKeyFromBytes(pubKeyBytes []byte) (pubKey PubKey, err error) { + err = wire.ReadBinaryBytes(pubKeyBytes, &pubKey) + return } -var pubKeyMapper data.Mapper +//---------------------------------------- -// register both public key types with go-data (and thus go-wire) -func init() { - pubKeyMapper = data.NewMapper(PubKeyS{}). - RegisterImplementation(PubKeyEd25519{}, NameEd25519, TypeEd25519). - RegisterImplementation(PubKeySecp256k1{}, NameSecp256k1, TypeSecp256k1) +type PubKey struct { + PubKeyInner `json:"unwrap"` } -// PubKeyS add json serialization to PubKey -type PubKeyS struct { - PubKey -} - -func WrapPubKey(pk PubKey) PubKeyS { - for ppk, ok := pk.(PubKeyS); ok; ppk, ok = pk.(PubKeyS) { - pk = ppk.PubKey - } - return PubKeyS{pk} +// DO NOT USE THIS INTERFACE. +// You probably want to use PubKey +type PubKeyInner interface { + AssertIsPubKeyInner() + Address() []byte + Bytes() []byte + KeyString() string + VerifyBytes(msg []byte, sig Signature) bool + Equals(PubKey) bool + Wrap() PubKey } -func (p PubKeyS) MarshalJSON() ([]byte, error) { - return pubKeyMapper.ToJSON(p.PubKey) +func (pk PubKey) MarshalJSON() ([]byte, error) { + return pubKeyMapper.ToJSON(pk.PubKeyInner) } -func (p *PubKeyS) UnmarshalJSON(data []byte) (err error) { +func (pk *PubKey) UnmarshalJSON(data []byte) (err error) { parsed, err := pubKeyMapper.FromJSON(data) if err == nil && parsed != nil { - p.PubKey = parsed.(PubKey) + pk.PubKeyInner = parsed.(PubKeyInner) } return } -func (p PubKeyS) Empty() bool { - return p.PubKey == nil +// Unwrap recovers the concrete interface safely (regardless of levels of embeds) +func (pk PubKey) Unwrap() PubKeyInner { + pkI := pk.PubKeyInner + for wrap, ok := pkI.(PubKey); ok; wrap, ok = pkI.(PubKey) { + pkI = wrap.PubKeyInner + } + return pkI } -func PubKeyFromBytes(pubKeyBytes []byte) (pubKey PubKey, err error) { - err = wire.ReadBinaryBytes(pubKeyBytes, &pubKey) - return +func (p PubKey) Empty() bool { + return p.PubKeyInner == nil } +var pubKeyMapper = data.NewMapper(PubKey{}). + RegisterImplementation(PubKeyEd25519{}, NameEd25519, TypeEd25519). + RegisterImplementation(PubKeySecp256k1{}, NameSecp256k1, TypeSecp256k1) + //------------------------------------- -// Implements PubKey +var _ PubKeyInner = PubKeyEd25519{} + +// Implements PubKeyInner type PubKeyEd25519 [32]byte +func (pubKey PubKeyEd25519) AssertIsPubKeyInner() {} + func (pubKey PubKeyEd25519) Address() []byte { w, n, err := new(bytes.Buffer), new(int), new(error) wire.WriteBinary(pubKey[:], w, n, err) @@ -83,16 +88,12 @@ func (pubKey PubKeyEd25519) Address() []byte { } func (pubKey PubKeyEd25519) Bytes() []byte { - return wire.BinaryBytes(struct{ PubKey }{pubKey}) + return wire.BinaryBytes(PubKey{pubKey}) } func (pubKey PubKeyEd25519) VerifyBytes(msg []byte, sig_ Signature) bool { - // unwrap if needed - if wrap, ok := sig_.(SignatureS); ok { - sig_ = wrap.Signature - } // make sure we use the same algorithm to sign - sig, ok := sig_.(SignatureEd25519) + sig, ok := sig_.Unwrap().(SignatureEd25519) if !ok { return false } @@ -134,20 +135,28 @@ func (pubKey PubKeyEd25519) KeyString() string { } func (pubKey PubKeyEd25519) Equals(other PubKey) bool { - if otherEd, ok := other.(PubKeyEd25519); ok { + if otherEd, ok := other.Unwrap().(PubKeyEd25519); ok { return bytes.Equal(pubKey[:], otherEd[:]) } else { return false } } +func (pubKey PubKeyEd25519) Wrap() PubKey { + return PubKey{pubKey} +} + //------------------------------------- +var _ PubKeyInner = PubKeySecp256k1{} + // Implements PubKey. // Compressed pubkey (just the x-cord), // prefixed with 0x02 or 0x03, depending on the y-cord. type PubKeySecp256k1 [33]byte +func (pubKey PubKeySecp256k1) AssertIsPubKeyInner() {} + // Implements Bitcoin style addresses: RIPEMD160(SHA256(pubkey)) func (pubKey PubKeySecp256k1) Address() []byte { hasherSHA256 := sha256.New() @@ -160,16 +169,12 @@ func (pubKey PubKeySecp256k1) Address() []byte { } func (pubKey PubKeySecp256k1) Bytes() []byte { - return wire.BinaryBytes(struct{ PubKey }{pubKey}) + return wire.BinaryBytes(PubKey{pubKey}) } func (pubKey PubKeySecp256k1) VerifyBytes(msg []byte, sig_ Signature) bool { - // unwrap if needed - if wrap, ok := sig_.(SignatureS); ok { - sig_ = wrap.Signature - } // and assert same algorithm to sign and verify - sig, ok := sig_.(SignatureSecp256k1) + sig, ok := sig_.Unwrap().(SignatureSecp256k1) if !ok { return false } @@ -207,9 +212,13 @@ func (pubKey PubKeySecp256k1) KeyString() string { } func (pubKey PubKeySecp256k1) Equals(other PubKey) bool { - if otherSecp, ok := other.(PubKeySecp256k1); ok { + if otherSecp, ok := other.Unwrap().(PubKeySecp256k1); ok { return bytes.Equal(pubKey[:], otherSecp[:]) } else { return false } } + +func (pubKey PubKeySecp256k1) Wrap() PubKey { + return PubKey{pubKey} +} diff --git a/pub_key_test.go b/pub_key_test.go index 0616f5546..31642233c 100644 --- a/pub_key_test.go +++ b/pub_key_test.go @@ -31,7 +31,7 @@ func TestPubKeySecp256k1Address(t *testing.T) { var priv PrivKeySecp256k1 copy(priv[:], privB) - pubT := priv.PubKey().(PubKeySecp256k1) + pubT := priv.PubKey().Unwrap().(PubKeySecp256k1) pub := pubT[:] addr := priv.PubKey().Address() diff --git a/random.go b/random.go index edadeaab6..40cbcf8fa 100644 --- a/random.go +++ b/random.go @@ -8,7 +8,7 @@ import ( "io" "sync" - . "github.com/tendermint/go-common" + . "github.com/tendermint/tmlibs/common" ) var gRandInfo *randInfo diff --git a/signature.go b/signature.go index bb9d82009..36451ed48 100644 --- a/signature.go +++ b/signature.go @@ -4,68 +4,73 @@ import ( "bytes" "fmt" - . "github.com/tendermint/go-common" - data "github.com/tendermint/go-data" + . "github.com/tendermint/tmlibs/common" + data "github.com/tendermint/go-wire/data" "github.com/tendermint/go-wire" ) -// Signature is a part of Txs and consensus Votes. -type Signature interface { - Bytes() []byte - IsZero() bool - String() string - Equals(Signature) bool +func SignatureFromBytes(sigBytes []byte) (sig Signature, err error) { + err = wire.ReadBinaryBytes(sigBytes, &sig) + return } -var sigMapper data.Mapper +//---------------------------------------- -// register both public key types with go-data (and thus go-wire) -func init() { - sigMapper = data.NewMapper(SignatureS{}). - RegisterImplementation(SignatureEd25519{}, NameEd25519, TypeEd25519). - RegisterImplementation(SignatureSecp256k1{}, NameSecp256k1, TypeSecp256k1) +type Signature struct { + SignatureInner `json:"unwrap"` } -// SignatureS add json serialization to Signature -type SignatureS struct { - Signature -} - -func WrapSignature(sig Signature) SignatureS { - for ssig, ok := sig.(SignatureS); ok; ssig, ok = sig.(SignatureS) { - sig = ssig.Signature - } - return SignatureS{sig} +// DO NOT USE THIS INTERFACE. +// You probably want to use Signature. +type SignatureInner interface { + AssertIsSignatureInner() + Bytes() []byte + IsZero() bool + String() string + Equals(Signature) bool + Wrap() Signature } -func (p SignatureS) MarshalJSON() ([]byte, error) { - return sigMapper.ToJSON(p.Signature) +func (sig Signature) MarshalJSON() ([]byte, error) { + return sigMapper.ToJSON(sig.SignatureInner) } -func (p *SignatureS) UnmarshalJSON(data []byte) (err error) { +func (sig *Signature) UnmarshalJSON(data []byte) (err error) { parsed, err := sigMapper.FromJSON(data) if err == nil && parsed != nil { - p.Signature = parsed.(Signature) + sig.SignatureInner = parsed.(SignatureInner) } return } -func (p SignatureS) Empty() bool { - return p.Signature == nil +// Unwrap recovers the concrete interface safely (regardless of levels of embeds) +func (sig Signature) Unwrap() SignatureInner { + pk := sig.SignatureInner + for wrap, ok := pk.(Signature); ok; wrap, ok = pk.(Signature) { + pk = wrap.SignatureInner + } + return pk } -func SignatureFromBytes(sigBytes []byte) (sig Signature, err error) { - err = wire.ReadBinaryBytes(sigBytes, &sig) - return +func (sig Signature) Empty() bool { + return sig.SignatureInner == nil } +var sigMapper = data.NewMapper(Signature{}). + RegisterImplementation(SignatureEd25519{}, NameEd25519, TypeEd25519). + RegisterImplementation(SignatureSecp256k1{}, NameSecp256k1, TypeSecp256k1) + //------------------------------------- +var _ SignatureInner = SignatureEd25519{} + // Implements Signature type SignatureEd25519 [64]byte +func (sig SignatureEd25519) AssertIsSignatureInner() {} + func (sig SignatureEd25519) Bytes() []byte { - return wire.BinaryBytes(struct{ Signature }{sig}) + return wire.BinaryBytes(Signature{sig}) } func (sig SignatureEd25519) IsZero() bool { return len(sig) == 0 } @@ -73,31 +78,39 @@ func (sig SignatureEd25519) IsZero() bool { return len(sig) == 0 } func (sig SignatureEd25519) String() string { return fmt.Sprintf("/%X.../", Fingerprint(sig[:])) } func (sig SignatureEd25519) Equals(other Signature) bool { - if otherEd, ok := other.(SignatureEd25519); ok { + if otherEd, ok := other.Unwrap().(SignatureEd25519); ok { return bytes.Equal(sig[:], otherEd[:]) } else { return false } } -func (p SignatureEd25519) MarshalJSON() ([]byte, error) { - return data.Encoder.Marshal(p[:]) +func (sig SignatureEd25519) MarshalJSON() ([]byte, error) { + return data.Encoder.Marshal(sig[:]) } -func (p *SignatureEd25519) UnmarshalJSON(enc []byte) error { +func (sig *SignatureEd25519) UnmarshalJSON(enc []byte) error { var ref []byte err := data.Encoder.Unmarshal(&ref, enc) - copy(p[:], ref) + copy(sig[:], ref) return err } +func (sig SignatureEd25519) Wrap() Signature { + return Signature{sig} +} + //------------------------------------- +var _ SignatureInner = SignatureSecp256k1{} + // Implements Signature type SignatureSecp256k1 []byte +func (sig SignatureSecp256k1) AssertIsSignatureInner() {} + func (sig SignatureSecp256k1) Bytes() []byte { - return wire.BinaryBytes(struct{ Signature }{sig}) + return wire.BinaryBytes(Signature{sig}) } func (sig SignatureSecp256k1) IsZero() bool { return len(sig) == 0 } @@ -105,16 +118,20 @@ func (sig SignatureSecp256k1) IsZero() bool { return len(sig) == 0 } func (sig SignatureSecp256k1) String() string { return fmt.Sprintf("/%X.../", Fingerprint(sig[:])) } func (sig SignatureSecp256k1) Equals(other Signature) bool { - if otherEd, ok := other.(SignatureSecp256k1); ok { + if otherEd, ok := other.Unwrap().(SignatureSecp256k1); ok { return bytes.Equal(sig[:], otherEd[:]) } else { return false } } -func (p SignatureSecp256k1) MarshalJSON() ([]byte, error) { - return data.Encoder.Marshal(p) +func (sig SignatureSecp256k1) MarshalJSON() ([]byte, error) { + return data.Encoder.Marshal(sig) +} + +func (sig *SignatureSecp256k1) UnmarshalJSON(enc []byte) error { + return data.Encoder.Unmarshal((*[]byte)(sig), enc) } -func (p *SignatureSecp256k1) UnmarshalJSON(enc []byte) error { - return data.Encoder.Unmarshal((*[]byte)(p), enc) +func (sig SignatureSecp256k1) Wrap() Signature { + return Signature{sig} } diff --git a/signature_test.go b/signature_test.go index f04262b77..5e9f06723 100644 --- a/signature_test.go +++ b/signature_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tendermint/ed25519" - data "github.com/tendermint/go-data" + data "github.com/tendermint/go-wire/data" ) func TestSignAndValidateEd25519(t *testing.T) { @@ -22,9 +22,9 @@ func TestSignAndValidateEd25519(t *testing.T) { assert.True(t, pubKey.VerifyBytes(msg, sig)) // Mutate the signature, just one bit. - sigEd := sig.(SignatureEd25519) - sigEd[0] ^= byte(0x01) - sig = Signature(sigEd) + sigEd := sig.Unwrap().(SignatureEd25519) + sigEd[7] ^= byte(0x01) + sig = sigEd.Wrap() assert.False(t, pubKey.VerifyBytes(msg, sig)) } @@ -39,28 +39,28 @@ func TestSignAndValidateSecp256k1(t *testing.T) { assert.True(t, pubKey.VerifyBytes(msg, sig)) // Mutate the signature, just one bit. - sigEd := sig.(SignatureSecp256k1) - sigEd[0] ^= byte(0x01) - sig = Signature(sigEd) + sigEd := sig.Unwrap().(SignatureSecp256k1) + sigEd[3] ^= byte(0x01) + sig = sigEd.Wrap() assert.False(t, pubKey.VerifyBytes(msg, sig)) } func TestSignatureEncodings(t *testing.T) { cases := []struct { - privKey PrivKeyS + privKey PrivKey sigSize int sigType byte sigName string }{ { - privKey: PrivKeyS{GenPrivKeyEd25519()}, + privKey: GenPrivKeyEd25519().Wrap(), sigSize: ed25519.SignatureSize, sigType: TypeEd25519, sigName: NameEd25519, }, { - privKey: PrivKeyS{GenPrivKeySecp256k1()}, + privKey: GenPrivKeySecp256k1().Wrap(), sigSize: 0, // unknown sigType: TypeSecp256k1, sigName: NameSecp256k1, @@ -69,10 +69,10 @@ func TestSignatureEncodings(t *testing.T) { for _, tc := range cases { // note we embed them from the beginning.... - pubKey := PubKeyS{tc.privKey.PubKey()} + pubKey := tc.privKey.PubKey() msg := CRandBytes(128) - sig := SignatureS{tc.privKey.Sign(msg)} + sig := tc.privKey.Sign(msg) // store as wire bin, err := data.ToWire(sig) @@ -83,7 +83,7 @@ func TestSignatureEncodings(t *testing.T) { assert.Equal(t, tc.sigType, bin[0]) // and back - sig2 := SignatureS{} + sig2 := Signature{} err = data.FromWire(bin, &sig2) require.Nil(t, err, "%+v", err) assert.EqualValues(t, sig, sig2) @@ -95,7 +95,7 @@ func TestSignatureEncodings(t *testing.T) { assert.True(t, strings.Contains(string(js), tc.sigName)) // and back - sig3 := SignatureS{} + sig3 := Signature{} err = data.FromJSON(js, &sig3) require.Nil(t, err, "%+v", err) assert.EqualValues(t, sig, sig3) @@ -118,25 +118,25 @@ func TestWrapping(t *testing.T) { sig := priv.Sign(msg) // do some wrapping - pubs := []PubKeyS{ - WrapPubKey(nil), - WrapPubKey(pub), - WrapPubKey(WrapPubKey(WrapPubKey(WrapPubKey(pub)))), - WrapPubKey(PubKeyS{PubKeyS{PubKeyS{pub}}}), + pubs := []PubKey{ + PubKey{nil}, + pub.Wrap(), + pub.Wrap().Wrap().Wrap(), + PubKey{PubKey{PubKey{pub}}}.Wrap(), } for _, p := range pubs { - _, ok := p.PubKey.(PubKeyS) + _, ok := p.PubKeyInner.(PubKey) assert.False(ok) } - sigs := []SignatureS{ - WrapSignature(nil), - WrapSignature(sig), - WrapSignature(WrapSignature(WrapSignature(WrapSignature(sig)))), - WrapSignature(SignatureS{SignatureS{SignatureS{sig}}}), + sigs := []Signature{ + Signature{nil}, + sig.Wrap(), + sig.Wrap().Wrap().Wrap(), + Signature{Signature{Signature{sig}}}.Wrap(), } for _, s := range sigs { - _, ok := s.Signature.(SignatureS) + _, ok := s.SignatureInner.(Signature) assert.False(ok) } diff --git a/symmetric.go b/symmetric.go index 36bddd44c..d4ac9b55b 100644 --- a/symmetric.go +++ b/symmetric.go @@ -3,7 +3,7 @@ package crypto import ( "errors" - . "github.com/tendermint/go-common" + . "github.com/tendermint/tmlibs/common" "golang.org/x/crypto/nacl/secretbox" )