From eeb53bc14301a54aee7cd7e1475e296155ee986d Mon Sep 17 00:00:00 2001 From: Steven Roose Date: Thu, 21 Dec 2017 11:36:05 +0100 Subject: [PATCH] cmd/ethkey: new command line tool for keys (#15438) ethkey is a new tool that serves as a command line interface to the basic key management functionalities of geth. It currently supports: - generating keyfiles - inspecting keyfiles (print public and private key) - signing messages - verifying signed messages --- cmd/ethkey/README.md | 41 ++++++++++++ cmd/ethkey/generate.go | 117 ++++++++++++++++++++++++++++++++ cmd/ethkey/inspect.go | 74 +++++++++++++++++++++ cmd/ethkey/main.go | 70 +++++++++++++++++++ cmd/ethkey/message.go | 148 +++++++++++++++++++++++++++++++++++++++++ cmd/ethkey/utils.go | 83 +++++++++++++++++++++++ 6 files changed, 533 insertions(+) create mode 100644 cmd/ethkey/README.md create mode 100644 cmd/ethkey/generate.go create mode 100644 cmd/ethkey/inspect.go create mode 100644 cmd/ethkey/main.go create mode 100644 cmd/ethkey/message.go create mode 100644 cmd/ethkey/utils.go diff --git a/cmd/ethkey/README.md b/cmd/ethkey/README.md new file mode 100644 index 0000000000..cf72ba43d7 --- /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 ` + +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 ` + +Sign the message with a keyfile. +It is possible to refer to a file containing the message. + + +### `ethkey verify
` + +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 0000000000..dee0e9d70e --- /dev/null +++ b/cmd/ethkey/generate.go @@ -0,0 +1,117 @@ +package main + +import ( + "crypto/ecdsa" + "crypto/rand" + "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: "[ ]", + Description: ` +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.`, + Flags: []cli.Flag{ + passphraseFlag, + jsonFlag, + cli.StringFlag{ + Name: "privatekey", + Usage: "the file from where to read the private key to " + + "generate a keyfile for", + }, + }, + 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 + + // First check if a private key file is provided. + privateKeyFile := ctx.String("privatekey") + if privateKeyFile != "" { + privateKeyBytes, err := ioutil.ReadFile(privateKeyFile) + if err != nil { + utils.Fatalf("Failed to read the private key file '%s': %v", + privateKeyFile, err) + } + + pk, err := crypto.HexToECDSA(string(privateKeyBytes)) + if err != nil { + utils.Fatalf( + "Could not construct ECDSA private key from file content: %v", + err) + } + privateKey = pk + } + + // If not loaded, generate random. + if privateKey == nil { + pk, err := ecdsa.GenerateKey(crypto.S256(), rand.Reader) + if err != nil { + utils.Fatalf("Failed to generate random private key: %v", err) + } + privateKey = pk + } + + // 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 0000000000..8a7aeef848 --- /dev/null +++ b/cmd/ethkey/inspect.go @@ -0,0 +1,74 @@ +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: "", + 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 0000000000..b9b7a18e05 --- /dev/null +++ b/cmd/ethkey/main.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 . + +package main + +import ( + "fmt" + "os" + + "github.com/ethereum/go-ethereum/cmd/utils" + "gopkg.in/urfave/cli.v1" +) + +const ( + defaultKeyfileName = "keyfile.json" +) + +var ( + gitCommit = "" // Git SHA1 commit hash of the release (set via linker flags) + + app *cli.App // the main app instance +) + +var ( // Commonly used command line flags. + 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", + } +) + +// Configure the app instance. +func init() { + app = utils.NewApp(gitCommit, "an Ethereum key manager") + app.Commands = []cli.Command{ + commandGenerate, + commandInspect, + commandSignMessage, + commandVerifyMessage, + } +} + +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 0000000000..ae6b6552d3 --- /dev/null +++ b/cmd/ethkey/message.go @@ -0,0 +1,148 @@ +package main + +import ( + "encoding/hex" + "fmt" + "io/ioutil" + "os" + "strings" + + "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 commandSignMessage = cli.Command{ + Name: "signmessage", + Usage: "sign a message", + ArgsUsage: " ", + Description: ` +Sign the message with a keyfile. +It is possible to refer to a file containing the message.`, + Flags: []cli.Flag{ + passphraseFlag, + jsonFlag, + }, + Action: func(ctx *cli.Context) error { + keyfilepath := ctx.Args().First() + message := []byte(ctx.Args().Get(1)) + + // Load the keyfile. + 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) + } + + if len(message) == 0 { + utils.Fatalf("A message must be provided") + } + // Read message if file. + if _, err := os.Stat(string(message)); err == nil { + message, err = ioutil.ReadFile(string(message)) + if err != nil { + utils.Fatalf("Failed to read the message file: %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: "
", + Description: ` +Verify the signature of the message. +It is possible to refer to a file containing the message.`, + Flags: []cli.Flag{ + jsonFlag, + }, + Action: func(ctx *cli.Context) error { + addressStr := ctx.Args().First() + signatureHex := ctx.Args().Get(1) + message := []byte(ctx.Args().Get(2)) + + // Determine whether it is a keyfile, public key or address. + 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) + } + + if len(message) == 0 { + utils.Fatalf("A message must be provided") + } + // Read message if file. + if _, err := os.Stat(string(message)); err == nil { + message, err = ioutil.ReadFile(string(message)) + if err != nil { + utils.Fatalf("Failed to read the message file: %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: strings.ToLower(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 + }, +} diff --git a/cmd/ethkey/utils.go b/cmd/ethkey/utils.go new file mode 100644 index 0000000000..0e563bf922 --- /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 . + +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)) +}