aboutsummaryrefslogtreecommitdiffstats
path: root/cmd
diff options
context:
space:
mode:
authorSteven Roose <stevenroose@gmail.com>2017-12-21 18:36:05 +0800
committerFelix Lange <fjl@users.noreply.github.com>2017-12-21 18:36:05 +0800
commiteeb53bc14301a54aee7cd7e1475e296155ee986d (patch)
treefcda4bda0bfec448817226ec28fc4786edcdaed7 /cmd
parente21aa0fda3b9d0b101d60d03e98a0bdd4d415dea (diff)
downloaddexon-eeb53bc14301a54aee7cd7e1475e296155ee986d.tar.gz
dexon-eeb53bc14301a54aee7cd7e1475e296155ee986d.tar.zst
dexon-eeb53bc14301a54aee7cd7e1475e296155ee986d.zip
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
Diffstat (limited to 'cmd')
-rw-r--r--cmd/ethkey/README.md41
-rw-r--r--cmd/ethkey/generate.go117
-rw-r--r--cmd/ethkey/inspect.go74
-rw-r--r--cmd/ethkey/main.go70
-rw-r--r--cmd/ethkey/message.go148
-rw-r--r--cmd/ethkey/utils.go83
6 files changed, 533 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..dee0e9d70
--- /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: "[ <keyfile> ]",
+ 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 000000000..8a7aeef84
--- /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: "<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..b9b7a18e0
--- /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 <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"
+)
+
+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 000000000..ae6b6552d
--- /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: "<keyfile> <message/file>",
+ 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: "<address> <signature> <message/file>",
+ 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 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))
+}