cmd/clef: add importraw feature to clef (#26058)
This adds a subcommand that imports a raw secp256k1 key into the keystore managed by clef.
This commit is contained in:
parent
33e23ee37d
commit
17744639da
117
cmd/clef/consolecmd_test.go
Normal file
117
cmd/clef/consolecmd_test.go
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
// Copyright 2022 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"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestImportRaw tests clef --importraw
|
||||||
|
func TestImportRaw(t *testing.T) {
|
||||||
|
keyPath := filepath.Join(os.TempDir(), fmt.Sprintf("%v-tempkey.test", t.Name()))
|
||||||
|
os.WriteFile(keyPath, []byte("0102030405060708090a0102030405060708090a0102030405060708090a0102"), 0777)
|
||||||
|
t.Cleanup(func() { os.Remove(keyPath) })
|
||||||
|
|
||||||
|
t.Parallel()
|
||||||
|
t.Run("happy-path", func(t *testing.T) {
|
||||||
|
// Run clef importraw
|
||||||
|
clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "importraw", keyPath)
|
||||||
|
clef.input("myverylongpassword").input("myverylongpassword")
|
||||||
|
if out := string(clef.Output()); !strings.Contains(out,
|
||||||
|
"Key imported:\n Address 0x9160DC9105f7De5dC5E7f3d97ef11DA47269BdA6") {
|
||||||
|
t.Logf("Output\n%v", out)
|
||||||
|
t.Error("Failure")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// tests clef --importraw with mismatched passwords.
|
||||||
|
t.Run("pw-mismatch", func(t *testing.T) {
|
||||||
|
// Run clef importraw
|
||||||
|
clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "importraw", keyPath)
|
||||||
|
clef.input("myverylongpassword1").input("myverylongpassword2").WaitExit()
|
||||||
|
if have, want := clef.StderrText(), "Passwords do not match\n"; have != want {
|
||||||
|
t.Errorf("have %q, want %q", have, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// tests clef --importraw with a too short password.
|
||||||
|
t.Run("short-pw", func(t *testing.T) {
|
||||||
|
// Run clef importraw
|
||||||
|
clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "importraw", keyPath)
|
||||||
|
clef.input("shorty").input("shorty").WaitExit()
|
||||||
|
if have, want := clef.StderrText(),
|
||||||
|
"password requirements not met: password too short (<10 characters)\n"; have != want {
|
||||||
|
t.Errorf("have %q, want %q", have, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestListAccounts tests clef --list-accounts
|
||||||
|
func TestListAccounts(t *testing.T) {
|
||||||
|
keyPath := filepath.Join(os.TempDir(), fmt.Sprintf("%v-tempkey.test", t.Name()))
|
||||||
|
os.WriteFile(keyPath, []byte("0102030405060708090a0102030405060708090a0102030405060708090a0102"), 0777)
|
||||||
|
t.Cleanup(func() { os.Remove(keyPath) })
|
||||||
|
|
||||||
|
t.Parallel()
|
||||||
|
t.Run("no-accounts", func(t *testing.T) {
|
||||||
|
clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "list-accounts")
|
||||||
|
if out := string(clef.Output()); !strings.Contains(out, "The keystore is empty.") {
|
||||||
|
t.Logf("Output\n%v", out)
|
||||||
|
t.Error("Failure")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("one-account", func(t *testing.T) {
|
||||||
|
// First, we need to import
|
||||||
|
clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "importraw", keyPath)
|
||||||
|
clef.input("myverylongpassword").input("myverylongpassword").WaitExit()
|
||||||
|
// Secondly, do a listing, using the same datadir
|
||||||
|
clef = runWithKeystore(t, clef.Datadir, "--suppress-bootwarn", "--lightkdf", "list-accounts")
|
||||||
|
if out := string(clef.Output()); !strings.Contains(out, "0x9160DC9105f7De5dC5E7f3d97ef11DA47269BdA6 (keystore:") {
|
||||||
|
t.Logf("Output\n%v", out)
|
||||||
|
t.Error("Failure")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestListWallets tests clef --list-wallets
|
||||||
|
func TestListWallets(t *testing.T) {
|
||||||
|
keyPath := filepath.Join(os.TempDir(), fmt.Sprintf("%v-tempkey.test", t.Name()))
|
||||||
|
os.WriteFile(keyPath, []byte("0102030405060708090a0102030405060708090a0102030405060708090a0102"), 0777)
|
||||||
|
t.Cleanup(func() { os.Remove(keyPath) })
|
||||||
|
|
||||||
|
t.Parallel()
|
||||||
|
t.Run("no-accounts", func(t *testing.T) {
|
||||||
|
clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "list-wallets")
|
||||||
|
if out := string(clef.Output()); !strings.Contains(out, "There are no wallets.") {
|
||||||
|
t.Logf("Output\n%v", out)
|
||||||
|
t.Error("Failure")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("one-account", func(t *testing.T) {
|
||||||
|
// First, we need to import
|
||||||
|
clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "importraw", keyPath)
|
||||||
|
clef.input("myverylongpassword").input("myverylongpassword").WaitExit()
|
||||||
|
// Secondly, do a listing, using the same datadir
|
||||||
|
clef = runWithKeystore(t, clef.Datadir, "--suppress-bootwarn", "--lightkdf", "list-wallets")
|
||||||
|
if out := string(clef.Output()); !strings.Contains(out, "Account 0: 0x9160DC9105f7De5dC5E7f3d97ef11DA47269BdA6") {
|
||||||
|
t.Logf("Output\n%v", out)
|
||||||
|
t.Error("Failure")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -23,6 +23,7 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math/big"
|
"math/big"
|
||||||
@ -74,7 +75,7 @@ PURPOSE. See the GNU General Public License for more details.
|
|||||||
var (
|
var (
|
||||||
logLevelFlag = &cli.IntFlag{
|
logLevelFlag = &cli.IntFlag{
|
||||||
Name: "loglevel",
|
Name: "loglevel",
|
||||||
Value: 4,
|
Value: 3,
|
||||||
Usage: "log level to emit to the screen",
|
Usage: "log level to emit to the screen",
|
||||||
}
|
}
|
||||||
advancedMode = &cli.BoolFlag{
|
advancedMode = &cli.BoolFlag{
|
||||||
@ -238,6 +239,23 @@ The gendoc generates example structures of the json-rpc communication types.
|
|||||||
Description: `
|
Description: `
|
||||||
Lists the wallets known to Clef.
|
Lists the wallets known to Clef.
|
||||||
`}
|
`}
|
||||||
|
importRawCommand = &cli.Command{
|
||||||
|
Action: accountImport,
|
||||||
|
Name: "importraw",
|
||||||
|
Usage: "Import a hex-encoded private key.",
|
||||||
|
ArgsUsage: "<keyfile>",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
logLevelFlag,
|
||||||
|
keystoreFlag,
|
||||||
|
utils.LightKDFFlag,
|
||||||
|
acceptFlag,
|
||||||
|
},
|
||||||
|
Description: `
|
||||||
|
Imports an unencrypted private key from <keyfile> and creates a new account.
|
||||||
|
Prints the address.
|
||||||
|
The keyfile is assumed to contain an unencrypted private key in hexadecimal format.
|
||||||
|
The account is saved in encrypted format, you are prompted for a password.
|
||||||
|
`}
|
||||||
)
|
)
|
||||||
|
|
||||||
var app = flags.NewApp("Manage Ethereum account operations")
|
var app = flags.NewApp("Manage Ethereum account operations")
|
||||||
@ -273,6 +291,7 @@ func init() {
|
|||||||
setCredentialCommand,
|
setCredentialCommand,
|
||||||
delCredentialCommand,
|
delCredentialCommand,
|
||||||
newAccountCommand,
|
newAccountCommand,
|
||||||
|
importRawCommand,
|
||||||
gendocCommand,
|
gendocCommand,
|
||||||
listAccountsCommand,
|
listAccountsCommand,
|
||||||
listWalletsCommand,
|
listWalletsCommand,
|
||||||
@ -378,9 +397,9 @@ func attestFile(ctx *cli.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func initInternalApi(c *cli.Context) (*core.UIServerAPI, error) {
|
func initInternalApi(c *cli.Context) (*core.UIServerAPI, core.UIClientAPI, error) {
|
||||||
if err := initialize(c); err != nil {
|
if err := initialize(c); err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
ui = core.NewCommandlineUI()
|
ui = core.NewCommandlineUI()
|
||||||
@ -391,7 +410,7 @@ func initInternalApi(c *cli.Context) (*core.UIServerAPI, error) {
|
|||||||
am := core.StartClefAccountManager(ksLoc, true, lightKdf, "")
|
am := core.StartClefAccountManager(ksLoc, true, lightKdf, "")
|
||||||
api := core.NewSignerAPI(am, 0, true, ui, nil, false, pwStorage)
|
api := core.NewSignerAPI(am, 0, true, ui, nil, false, pwStorage)
|
||||||
internalApi := core.NewUIServerAPI(api)
|
internalApi := core.NewUIServerAPI(api)
|
||||||
return internalApi, nil
|
return internalApi, ui, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setCredential(ctx *cli.Context) error {
|
func setCredential(ctx *cli.Context) error {
|
||||||
@ -478,7 +497,7 @@ func initialize(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newAccount(c *cli.Context) error {
|
func newAccount(c *cli.Context) error {
|
||||||
internalApi, err := initInternalApi(c)
|
internalApi, _, err := initInternalApi(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -490,7 +509,7 @@ func newAccount(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func listAccounts(c *cli.Context) error {
|
func listAccounts(c *cli.Context) error {
|
||||||
internalApi, err := initInternalApi(c)
|
internalApi, _, err := initInternalApi(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -509,7 +528,7 @@ func listAccounts(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func listWallets(c *cli.Context) error {
|
func listWallets(c *cli.Context) error {
|
||||||
internalApi, err := initInternalApi(c)
|
internalApi, _, err := initInternalApi(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -528,6 +547,57 @@ func listWallets(c *cli.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// accountImport imports a raw hexadecimal private key via CLI.
|
||||||
|
func accountImport(c *cli.Context) error {
|
||||||
|
if c.Args().Len() != 1 {
|
||||||
|
return errors.New("<keyfile> must be given as first argument.")
|
||||||
|
}
|
||||||
|
internalApi, ui, err := initInternalApi(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pKey, err := crypto.LoadECDSA(c.Args().First())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
readPw := func(prompt string) (string, error) {
|
||||||
|
resp, err := ui.OnInputRequired(core.UserInputRequest{
|
||||||
|
Title: "Password",
|
||||||
|
Prompt: prompt,
|
||||||
|
IsPassword: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return resp.Text, nil
|
||||||
|
}
|
||||||
|
first, err := readPw("Please enter a password for the imported account")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
second, err := readPw("Please repeat the password you just entered")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if first != second {
|
||||||
|
return errors.New("Passwords do not match")
|
||||||
|
}
|
||||||
|
acc, err := internalApi.ImportRawKey(hex.EncodeToString(crypto.FromECDSA(pKey)), first)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ui.ShowInfo(fmt.Sprintf(`Key imported:
|
||||||
|
Address %v
|
||||||
|
Keystore file: %v
|
||||||
|
|
||||||
|
The key is now encrypted; losing the password will result in permanently losing
|
||||||
|
access to the key and all associated funds!
|
||||||
|
|
||||||
|
Make sure to backup keystore and passwords in a safe location.`,
|
||||||
|
acc.Address, acc.URL.Path))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ipcEndpoint resolves an IPC endpoint based on a configured value, taking into
|
// ipcEndpoint resolves an IPC endpoint based on a configured value, taking into
|
||||||
// account the set data folders as well as the designated platform we're currently
|
// account the set data folders as well as the designated platform we're currently
|
||||||
// running on.
|
// running on.
|
||||||
|
109
cmd/clef/run_test.go
Normal file
109
cmd/clef/run_test.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
// Copyright 2022 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
const registeredName = "clef-test"
|
||||||
|
|
||||||
|
type testproc struct {
|
||||||
|
*cmdtest.TestCmd
|
||||||
|
|
||||||
|
// template variables for expect
|
||||||
|
Datadir string
|
||||||
|
Etherbase string
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
reexec.Register(registeredName, func() {
|
||||||
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
// check if we have been reexec'd
|
||||||
|
if reexec.Init() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
// runClef spawns clef with the given command line args and adds keystore arg.
|
||||||
|
// This method creates a temporary keystore folder which will be removed after
|
||||||
|
// the test exits.
|
||||||
|
func runClef(t *testing.T, args ...string) *testproc {
|
||||||
|
ddir, err := os.MkdirTemp("", "cleftest-*")
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
os.RemoveAll(ddir)
|
||||||
|
})
|
||||||
|
return runWithKeystore(t, ddir, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// runWithKeystore spawns clef with the given command line args and adds keystore arg.
|
||||||
|
// This method does _not_ create the keystore folder, but it _does_ add the arg
|
||||||
|
// to the args.
|
||||||
|
func runWithKeystore(t *testing.T, keystore string, args ...string) *testproc {
|
||||||
|
args = append([]string{"--keystore", keystore}, args...)
|
||||||
|
tt := &testproc{Datadir: keystore}
|
||||||
|
tt.TestCmd = cmdtest.NewTestCmd(t, tt)
|
||||||
|
// Boot "clef". This actually runs the test binary but the TestMain
|
||||||
|
// function will prevent any tests from running.
|
||||||
|
tt.Run(registeredName, args...)
|
||||||
|
return tt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (proc *testproc) input(text string) *testproc {
|
||||||
|
proc.TestCmd.InputLine(text)
|
||||||
|
return proc
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// waitForEndpoint waits for the rpc endpoint to appear, or
|
||||||
|
// aborts after 3 seconds.
|
||||||
|
func (proc *testproc) waitForEndpoint(t *testing.T) *testproc {
|
||||||
|
t.Helper()
|
||||||
|
timeout := 3 * time.Second
|
||||||
|
ipc := filepath.Join(proc.Datadir, "clef.ipc")
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
for time.Since(start) < timeout {
|
||||||
|
if _, err := os.Stat(ipc); !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Logf("endpoint %v opened", ipc)
|
||||||
|
return proc
|
||||||
|
}
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
}
|
||||||
|
t.Logf("stderr: \n%v", proc.StderrText())
|
||||||
|
t.Logf("stdout: \n%v", proc.Output())
|
||||||
|
t.Fatal("endpoint", ipc, "did not open within", timeout)
|
||||||
|
return proc
|
||||||
|
}
|
||||||
|
*/
|
Loading…
Reference in New Issue
Block a user