diff options
Diffstat (limited to 'cmd/ethkey')
-rw-r--r-- | cmd/ethkey/README.md | 41 | ||||
-rw-r--r-- | cmd/ethkey/generate.go | 118 | ||||
-rw-r--r-- | cmd/ethkey/inspect.go | 75 | ||||
-rw-r--r-- | cmd/ethkey/main.go | 67 | ||||
-rw-r--r-- | cmd/ethkey/message.go | 159 | ||||
-rw-r--r-- | cmd/ethkey/message_test.go | 70 | ||||
-rw-r--r-- | cmd/ethkey/run_test.go | 54 | ||||
-rw-r--r-- | cmd/ethkey/utils.go | 83 |
8 files changed, 667 insertions, 0 deletions
diff --git a/cmd/ethkey/README.md b/cmd/ethkey/README.md new file mode 100644 index 000000000..cf72ba43d --- /dev/null +++ b/cmd/ethkey/README.md @@ -0,0 +1,41 @@ +ethkey +====== + +ethkey is a simple command-line tool for working with Ethereum keyfiles. + + +# Usage + +### `ethkey generate` + +Generate a new keyfile. +If you want to use an existing private key to use in the keyfile, it can be +specified by setting `--privatekey` with the location of the file containing the +private key. + + +### `ethkey inspect <keyfile>` + +Print various information about the keyfile. +Private key information can be printed by using the `--private` flag; +make sure to use this feature with great caution! + + +### `ethkey sign <keyfile> <message/file>` + +Sign the message with a keyfile. +It is possible to refer to a file containing the message. + + +### `ethkey verify <address> <signature> <message/file>` + +Verify the signature of the message. +It is possible to refer to a file containing the message. + + +## Passphrases + +For every command that uses a keyfile, you will be prompted to provide the +passphrase for decrypting the keyfile. To avoid this message, it is possible +to pass the passphrase by using the `--passphrase` flag pointing to a file that +contains the passphrase. diff --git a/cmd/ethkey/generate.go b/cmd/ethkey/generate.go new file mode 100644 index 000000000..6d57d17fb --- /dev/null +++ b/cmd/ethkey/generate.go @@ -0,0 +1,118 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. + +package main + +import ( + "crypto/ecdsa" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/cmd/utils" + "github.com/ethereum/go-ethereum/crypto" + "github.com/pborman/uuid" + "gopkg.in/urfave/cli.v1" +) + +type outputGenerate struct { + Address string + AddressEIP55 string +} + +var commandGenerate = cli.Command{ + Name: "generate", + Usage: "generate new keyfile", + ArgsUsage: "[ <keyfile> ]", + Description: ` +Generate a new keyfile. + +If you want to encrypt an existing private key, it can be specified by setting +--privatekey with the location of the file containing the private key. +`, + Flags: []cli.Flag{ + passphraseFlag, + jsonFlag, + cli.StringFlag{ + Name: "privatekey", + Usage: "file containing a raw private key to encrypt", + }, + }, + Action: func(ctx *cli.Context) error { + // Check if keyfile path given and make sure it doesn't already exist. + keyfilepath := ctx.Args().First() + if keyfilepath == "" { + keyfilepath = defaultKeyfileName + } + if _, err := os.Stat(keyfilepath); err == nil { + utils.Fatalf("Keyfile already exists at %s.", keyfilepath) + } else if !os.IsNotExist(err) { + utils.Fatalf("Error checking if keyfile exists: %v", err) + } + + var privateKey *ecdsa.PrivateKey + var err error + if file := ctx.String("privatekey"); file != "" { + // Load private key from file. + privateKey, err = crypto.LoadECDSA(file) + if err != nil { + utils.Fatalf("Can't load private key: %v", err) + } + } else { + // If not loaded, generate random. + privateKey, err = crypto.GenerateKey() + if err != nil { + utils.Fatalf("Failed to generate random private key: %v", err) + } + } + + // Create the keyfile object with a random UUID. + id := uuid.NewRandom() + key := &keystore.Key{ + Id: id, + Address: crypto.PubkeyToAddress(privateKey.PublicKey), + PrivateKey: privateKey, + } + + // Encrypt key with passphrase. + passphrase := getPassPhrase(ctx, true) + keyjson, err := keystore.EncryptKey(key, passphrase, keystore.StandardScryptN, keystore.StandardScryptP) + if err != nil { + utils.Fatalf("Error encrypting key: %v", err) + } + + // Store the file to disk. + if err := os.MkdirAll(filepath.Dir(keyfilepath), 0700); err != nil { + utils.Fatalf("Could not create directory %s", filepath.Dir(keyfilepath)) + } + if err := ioutil.WriteFile(keyfilepath, keyjson, 0600); err != nil { + utils.Fatalf("Failed to write keyfile to %s: %v", keyfilepath, err) + } + + // Output some information. + out := outputGenerate{ + Address: key.Address.Hex(), + } + if ctx.Bool(jsonFlag.Name) { + mustPrintJSON(out) + } else { + fmt.Println("Address:", out.Address) + } + return nil + }, +} diff --git a/cmd/ethkey/inspect.go b/cmd/ethkey/inspect.go new file mode 100644 index 000000000..219a5460b --- /dev/null +++ b/cmd/ethkey/inspect.go @@ -0,0 +1,75 @@ +package main + +import ( + "encoding/hex" + "fmt" + "io/ioutil" + + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/cmd/utils" + "github.com/ethereum/go-ethereum/crypto" + "gopkg.in/urfave/cli.v1" +) + +type outputInspect struct { + Address string + PublicKey string + PrivateKey string +} + +var commandInspect = cli.Command{ + Name: "inspect", + Usage: "inspect a keyfile", + ArgsUsage: "<keyfile>", + Description: ` +Print various information about the keyfile. + +Private key information can be printed by using the --private flag; +make sure to use this feature with great caution!`, + Flags: []cli.Flag{ + passphraseFlag, + jsonFlag, + cli.BoolFlag{ + Name: "private", + Usage: "include the private key in the output", + }, + }, + Action: func(ctx *cli.Context) error { + keyfilepath := ctx.Args().First() + + // Read key from file. + keyjson, err := ioutil.ReadFile(keyfilepath) + if err != nil { + utils.Fatalf("Failed to read the keyfile at '%s': %v", keyfilepath, err) + } + + // Decrypt key with passphrase. + passphrase := getPassPhrase(ctx, false) + key, err := keystore.DecryptKey(keyjson, passphrase) + if err != nil { + utils.Fatalf("Error decrypting key: %v", err) + } + + // Output all relevant information we can retrieve. + showPrivate := ctx.Bool("private") + out := outputInspect{ + Address: key.Address.Hex(), + PublicKey: hex.EncodeToString( + crypto.FromECDSAPub(&key.PrivateKey.PublicKey)), + } + if showPrivate { + out.PrivateKey = hex.EncodeToString(crypto.FromECDSA(key.PrivateKey)) + } + + if ctx.Bool(jsonFlag.Name) { + mustPrintJSON(out) + } else { + fmt.Println("Address: ", out.Address) + fmt.Println("Public key: ", out.PublicKey) + if showPrivate { + fmt.Println("Private key: ", out.PrivateKey) + } + } + return nil + }, +} diff --git a/cmd/ethkey/main.go b/cmd/ethkey/main.go new file mode 100644 index 000000000..2a9e5ee48 --- /dev/null +++ b/cmd/ethkey/main.go @@ -0,0 +1,67 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. + +package main + +import ( + "fmt" + "os" + + "github.com/ethereum/go-ethereum/cmd/utils" + "gopkg.in/urfave/cli.v1" +) + +const ( + defaultKeyfileName = "keyfile.json" +) + +// Git SHA1 commit hash of the release (set via linker flags) +var gitCommit = "" + +var app *cli.App + +func init() { + app = utils.NewApp(gitCommit, "an Ethereum key manager") + app.Commands = []cli.Command{ + commandGenerate, + commandInspect, + commandSignMessage, + commandVerifyMessage, + } +} + +// Commonly used command line flags. +var ( + passphraseFlag = cli.StringFlag{ + Name: "passwordfile", + Usage: "the file that contains the passphrase for the keyfile", + } + jsonFlag = cli.BoolFlag{ + Name: "json", + Usage: "output JSON instead of human-readable format", + } + messageFlag = cli.StringFlag{ + Name: "message", + Usage: "the file that contains the message to sign/verify", + } +) + +func main() { + if err := app.Run(os.Args); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/cmd/ethkey/message.go b/cmd/ethkey/message.go new file mode 100644 index 000000000..531a931c8 --- /dev/null +++ b/cmd/ethkey/message.go @@ -0,0 +1,159 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. + +package main + +import ( + "encoding/hex" + "fmt" + "io/ioutil" + + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/cmd/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "gopkg.in/urfave/cli.v1" +) + +type outputSign struct { + Signature string +} + +var msgfileFlag = cli.StringFlag{ + Name: "msgfile", + Usage: "file containing the message to sign/verify", +} + +var commandSignMessage = cli.Command{ + Name: "signmessage", + Usage: "sign a message", + ArgsUsage: "<keyfile> <message>", + Description: ` +Sign the message with a keyfile. + +To sign a message contained in a file, use the --msgfile flag. +`, + Flags: []cli.Flag{ + passphraseFlag, + jsonFlag, + msgfileFlag, + }, + Action: func(ctx *cli.Context) error { + message := getMessage(ctx, 1) + + // Load the keyfile. + keyfilepath := ctx.Args().First() + keyjson, err := ioutil.ReadFile(keyfilepath) + if err != nil { + utils.Fatalf("Failed to read the keyfile at '%s': %v", keyfilepath, err) + } + + // Decrypt key with passphrase. + passphrase := getPassPhrase(ctx, false) + key, err := keystore.DecryptKey(keyjson, passphrase) + if err != nil { + utils.Fatalf("Error decrypting key: %v", err) + } + + signature, err := crypto.Sign(signHash(message), key.PrivateKey) + if err != nil { + utils.Fatalf("Failed to sign message: %v", err) + } + out := outputSign{Signature: hex.EncodeToString(signature)} + if ctx.Bool(jsonFlag.Name) { + mustPrintJSON(out) + } else { + fmt.Println("Signature:", out.Signature) + } + return nil + }, +} + +type outputVerify struct { + Success bool + RecoveredAddress string + RecoveredPublicKey string +} + +var commandVerifyMessage = cli.Command{ + Name: "verifymessage", + Usage: "verify the signature of a signed message", + ArgsUsage: "<address> <signature> <message>", + Description: ` +Verify the signature of the message. +It is possible to refer to a file containing the message.`, + Flags: []cli.Flag{ + jsonFlag, + msgfileFlag, + }, + Action: func(ctx *cli.Context) error { + addressStr := ctx.Args().First() + signatureHex := ctx.Args().Get(1) + message := getMessage(ctx, 2) + + if !common.IsHexAddress(addressStr) { + utils.Fatalf("Invalid address: %s", addressStr) + } + address := common.HexToAddress(addressStr) + signature, err := hex.DecodeString(signatureHex) + if err != nil { + utils.Fatalf("Signature encoding is not hexadecimal: %v", err) + } + + recoveredPubkey, err := crypto.SigToPub(signHash(message), signature) + if err != nil || recoveredPubkey == nil { + utils.Fatalf("Signature verification failed: %v", err) + } + recoveredPubkeyBytes := crypto.FromECDSAPub(recoveredPubkey) + recoveredAddress := crypto.PubkeyToAddress(*recoveredPubkey) + success := address == recoveredAddress + + out := outputVerify{ + Success: success, + RecoveredPublicKey: hex.EncodeToString(recoveredPubkeyBytes), + RecoveredAddress: recoveredAddress.Hex(), + } + if ctx.Bool(jsonFlag.Name) { + mustPrintJSON(out) + } else { + if out.Success { + fmt.Println("Signature verification successful!") + } else { + fmt.Println("Signature verification failed!") + } + fmt.Println("Recovered public key:", out.RecoveredPublicKey) + fmt.Println("Recovered address:", out.RecoveredAddress) + } + return nil + }, +} + +func getMessage(ctx *cli.Context, msgarg int) []byte { + if file := ctx.String("msgfile"); file != "" { + if len(ctx.Args()) > msgarg { + utils.Fatalf("Can't use --msgfile and message argument at the same time.") + } + msg, err := ioutil.ReadFile(file) + if err != nil { + utils.Fatalf("Can't read message file: %v", err) + } + return msg + } else if len(ctx.Args()) == msgarg+1 { + return []byte(ctx.Args().Get(msgarg)) + } + utils.Fatalf("Invalid number of arguments: want %d, got %d", msgarg+1, len(ctx.Args())) + return nil +} diff --git a/cmd/ethkey/message_test.go b/cmd/ethkey/message_test.go new file mode 100644 index 000000000..fb16f03d0 --- /dev/null +++ b/cmd/ethkey/message_test.go @@ -0,0 +1,70 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. + +package main + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +func TestMessageSignVerify(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "ethkey-test") + if err != nil { + t.Fatal("Can't create temporary directory:", err) + } + defer os.RemoveAll(tmpdir) + + keyfile := filepath.Join(tmpdir, "the-keyfile") + message := "test message" + + // Create the key. + generate := runEthkey(t, "generate", keyfile) + generate.Expect(` +!! Unsupported terminal, password will be echoed. +Passphrase: {{.InputLine "foobar"}} +Repeat passphrase: {{.InputLine "foobar"}} +`) + _, matches := generate.ExpectRegexp(`Address: (0x[0-9a-fA-F]{40})\n`) + address := matches[1] + generate.ExpectExit() + + // Sign a message. + sign := runEthkey(t, "signmessage", keyfile, message) + sign.Expect(` +!! Unsupported terminal, password will be echoed. +Passphrase: {{.InputLine "foobar"}} +`) + _, matches = sign.ExpectRegexp(`Signature: ([0-9a-f]+)\n`) + signature := matches[1] + sign.ExpectExit() + + // Verify the message. + verify := runEthkey(t, "verifymessage", address, signature, message) + _, matches = verify.ExpectRegexp(` +Signature verification successful! +Recovered public key: [0-9a-f]+ +Recovered address: (0x[0-9a-fA-F]{40}) +`) + recovered := matches[1] + verify.ExpectExit() + + if recovered != address { + t.Error("recovered address doesn't match generated key") + } +} diff --git a/cmd/ethkey/run_test.go b/cmd/ethkey/run_test.go new file mode 100644 index 000000000..8ce4fe5cd --- /dev/null +++ b/cmd/ethkey/run_test.go @@ -0,0 +1,54 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. + +package main + +import ( + "fmt" + "os" + "testing" + + "github.com/docker/docker/pkg/reexec" + "github.com/ethereum/go-ethereum/internal/cmdtest" +) + +type testEthkey struct { + *cmdtest.TestCmd +} + +// spawns ethkey with the given command line args. +func runEthkey(t *testing.T, args ...string) *testEthkey { + tt := new(testEthkey) + tt.TestCmd = cmdtest.NewTestCmd(t, tt) + tt.Run("ethkey-test", args...) + return tt +} + +func TestMain(m *testing.M) { + // Run the app if we've been exec'd as "ethkey-test" in runEthkey. + reexec.Register("ethkey-test", func() { + if err := app.Run(os.Args); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + os.Exit(0) + }) + // check if we have been reexec'd + if reexec.Init() { + return + } + os.Exit(m.Run()) +} diff --git a/cmd/ethkey/utils.go b/cmd/ethkey/utils.go new file mode 100644 index 000000000..0e563bf92 --- /dev/null +++ b/cmd/ethkey/utils.go @@ -0,0 +1,83 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. + +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "strings" + + "github.com/ethereum/go-ethereum/cmd/utils" + "github.com/ethereum/go-ethereum/console" + "github.com/ethereum/go-ethereum/crypto" + "gopkg.in/urfave/cli.v1" +) + +// getPassPhrase obtains a passphrase given by the user. It first checks the +// --passphrase command line flag and ultimately prompts the user for a +// passphrase. +func getPassPhrase(ctx *cli.Context, confirmation bool) string { + // Look for the --passphrase flag. + passphraseFile := ctx.String(passphraseFlag.Name) + if passphraseFile != "" { + content, err := ioutil.ReadFile(passphraseFile) + if err != nil { + utils.Fatalf("Failed to read passphrase file '%s': %v", + passphraseFile, err) + } + return strings.TrimRight(string(content), "\r\n") + } + + // Otherwise prompt the user for the passphrase. + passphrase, err := console.Stdin.PromptPassword("Passphrase: ") + if err != nil { + utils.Fatalf("Failed to read passphrase: %v", err) + } + if confirmation { + confirm, err := console.Stdin.PromptPassword("Repeat passphrase: ") + if err != nil { + utils.Fatalf("Failed to read passphrase confirmation: %v", err) + } + if passphrase != confirm { + utils.Fatalf("Passphrases do not match") + } + } + return passphrase +} + +// signHash is a helper function that calculates a hash for the given message +// that can be safely used to calculate a signature from. +// +// The hash is calulcated as +// keccak256("\x19Ethereum Signed Message:\n"${message length}${message}). +// +// This gives context to the signed message and prevents signing of transactions. +func signHash(data []byte) []byte { + msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data) + return crypto.Keccak256([]byte(msg)) +} + +// mustPrintJSON prints the JSON encoding of the given object and +// exits the program with an error message when the marshaling fails. +func mustPrintJSON(jsonObject interface{}) { + str, err := json.MarshalIndent(jsonObject, "", " ") + if err != nil { + utils.Fatalf("Failed to marshal JSON object: %v", err) + } + fmt.Println(string(str)) +} |