From fad33b193653e2462e557d72db0ab21fd79ff7e1 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Fri, 24 Nov 2017 12:11:17 +0100 Subject: [PATCH] signer: refactored request/response, made use of urfave.cli --- cmd/signer/api.go | 115 +++++++++++++++++++++++++++++--------------- cmd/signer/cliui.go | 95 +++++++++++++++++++++++++++--------- cmd/signer/main.go | 83 +++++++++++++++++++++----------- 3 files changed, 202 insertions(+), 91 deletions(-) diff --git a/cmd/signer/api.go b/cmd/signer/api.go index 82da94a2cb..b43ab2e830 100644 --- a/cmd/signer/api.go +++ b/cmd/signer/api.go @@ -49,74 +49,113 @@ type Metadata struct { scheme string } -// The SignTxRequest contains info about a transaction tos sign +type credPreference int + +const ( + forgetPw credPreference = iota + rememberPwNow +) + +type Credentials struct { + password string +} + +// SignTxRequest contains info about a transaction to sign type SignTxRequest struct { transaction *types.Transaction from accounts.Account } +type SignTxResponse struct { + hash common.Hash + approved bool + pw string +} type ExportRequest struct { account accounts.Account file string } +type ExportResponse struct { + approved bool +} + type ImportRequest struct { account accounts.Account } + +type ImportResponse struct { + approved bool + oldPassword string + newPassword string +} + type SignDataRequest struct { account accounts.Account rawdata hexutil.Bytes message string hash hexutil.Bytes } -type ApprovalStatus struct { - hash common.Hash +type SignDataResponse struct { approved bool pw string } type NewAccountRequest struct{} +type NewAccountResponse struct { + approved bool + pw string +} type ListRequest struct { accounts []Account } -type ListApproval struct { +type ListResponse struct { accounts []Account } // SignerUI specifies what method a UI needs to implement to be able to be used as a UI // for the signer type SignerUI interface { - ApproveTx(request *SignTxRequest, metadata Metadata, ch chan ApprovalStatus) - ApproveSignData(request *SignDataRequest, metadata Metadata, ch chan ApprovalStatus) - ApproveExport(request *ExportRequest, metadata Metadata, ch chan ApprovalStatus) - ApproveImport(request *ImportRequest, metadata Metadata, ch chan ApprovalStatus) - ApproveListing(request *ListRequest, metadata Metadata, ch chan ListApproval) - ApproveNewAccount(requst *NewAccountRequest, metadata Metadata, ch chan bool) - // In case signing fails, bad password etc + + // ApproveTx prompt the user for confirmation to request to sign transaction + ApproveTx(request *SignTxRequest, metadata Metadata, ch chan SignTxResponse) + // ApproveSignData prompt the user for confirmation to request to sign data + ApproveSignData(request *SignDataRequest, metadata Metadata, ch chan SignDataResponse) + // ApproveExport prompt the user for confirmation to export encrypted account json + ApproveExport(request *ExportRequest, metadata Metadata, ch chan ExportResponse) + // ApproveImport prompt the user for confirmation to import account json + ApproveImport(request *ImportRequest, metadata Metadata, ch chan ImportResponse) + // ApproveListing prompt the user for confirmation to list accounts + // the list of accounts to list can be modified by the ui + ApproveListing(request *ListRequest, metadata Metadata, ch chan ListResponse) + // ApproveNewAccount prompt the user for confirmation to create new account, and reveal to caller + ApproveNewAccount(requst *NewAccountRequest, metadata Metadata, ch chan NewAccountResponse) + // ShowError displays error message to user ShowError(message string) + // ShowInfo displays info message to user ShowInfo(message string) } type HeadlessUI struct { } -func (ui *HeadlessUI) ApproveTx(request *SignTxRequest, metadata Metadata, ch chan ApprovalStatus) { - ch <- ApprovalStatus{request.transaction.Hash(), true, ""} +func (ui *HeadlessUI) ApproveTx(request *SignTxRequest, metadata Metadata, ch chan SignTxResponse) { + ch <- SignTxResponse{request.transaction.Hash(), true, ""} } -func (ui *HeadlessUI) ApproveSignData(request *SignDataRequest, metadata Metadata, ch chan ApprovalStatus) { - ch <- ApprovalStatus{common.Hash{}, true, ""} +func (ui *HeadlessUI) ApproveSignData(request *SignDataRequest, metadata Metadata, ch chan SignDataResponse) { + ch <- SignDataResponse{true, ""} } -func (ui *HeadlessUI) ApproveExport(request *ExportRequest, metadata Metadata, ch chan ApprovalStatus) { - ch <- ApprovalStatus{common.Hash{}, true, ""} +func (ui *HeadlessUI) ApproveExport(request *ExportRequest, metadata Metadata, ch chan ExportResponse) { + ch <- ExportResponse{true} } -func (ui *HeadlessUI) ApproveImport(request *ImportRequest, metadata Metadata, ch chan ApprovalStatus) { - ch <- ApprovalStatus{common.Hash{}, true, ""} +func (ui *HeadlessUI) ApproveImport(request *ImportRequest, metadata Metadata, ch chan ImportResponse) { + ch <- ImportResponse{true, "", ""} } -func (ui *HeadlessUI) ApproveListing(request *ListRequest, metadata Metadata, ch chan ListApproval) { - ch <- ListApproval{request.accounts} +func (ui *HeadlessUI) ApproveListing(request *ListRequest, metadata Metadata, ch chan ListResponse) { + ch <- ListResponse{request.accounts} } -func (ui *HeadlessUI) ApproveNewAccount(requst *NewAccountRequest, metadata Metadata, ch chan bool) { - ch <- true +func (ui *HeadlessUI) ApproveNewAccount(requst *NewAccountRequest, metadata Metadata, ch chan ImportResponse) { + ch <- ImportResponse{true, "", ""} } func (ui *HeadlessUI) ShowError(message string) { //stdout is used by communication @@ -163,13 +202,13 @@ func NewSignerAPI(chainID int64, ksLocation string, noUSB bool, ui SignerUI) *Si func metaData(ctx context.Context) Metadata { m := Metadata{"NA", "NA", "NA"} - if v := ctx.Value("remote"); v != nil{ + if v := ctx.Value("remote"); v != nil { m.remote = v.(string) } - if v := ctx.Value("scheme"); v != nil{ + if v := ctx.Value("scheme"); v != nil { m.scheme = v.(string) } - if v := ctx.Value("local"); v != nil{ + if v := ctx.Value("local"); v != nil { m.local = v.(string) } return m @@ -179,7 +218,7 @@ func metaData(ctx context.Context) Metadata { // multiple accounts. func (api *SignerAPI) List(ctx context.Context) ([]Account, error) { - ch := make(chan ListApproval, 1) + ch := make(chan ListResponse, 1) var accounts []Account for _, wallet := range api.am.Wallets() { @@ -204,10 +243,10 @@ func (api *SignerAPI) New(ctx context.Context, passphrase string) (accounts.Acco if len(be) == 0 { return accounts.Account{}, errors.New("password based accounts not supported") } - ch := make(chan bool, 1) + ch := make(chan NewAccountResponse, 1) api.ui.ApproveNewAccount(&NewAccountRequest{}, metaData(ctx), ch) - if <-ch { + if resp := <-ch; resp.approved { return be[0].(*keystore.KeyStore).NewAccount(passphrase) } return accounts.Account{}, fmt.Errorf("Request denied") @@ -230,7 +269,7 @@ func (api *SignerAPI) SignTransaction(ctx context.Context, from common.Address, tx = types.NewTransaction(uint64(*args.Nonce), *args.To, (*big.Int)(args.Value), (*big.Int)(args.Gas), (*big.Int)(args.GasPrice), args.Data) } - ch := make(chan ApprovalStatus, 1) + ch := make(chan SignTxResponse, 1) api.ui.ApproveTx(&SignTxRequest{transaction: tx, from: acc}, metaData(ctx), ch) if result := <-ch; result.approved { @@ -266,10 +305,9 @@ func (api *SignerAPI) Sign(ctx context.Context, addr common.Address, passwd stri if err != nil { return nil, err } - ch := make(chan ApprovalStatus, 1) + ch := make(chan SignDataResponse, 1) - msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data) - sighash := crypto.Keccak256([]byte(msg)) + sighash, msg := signHash(data) api.ui.ApproveSignData(&SignDataRequest{account: account, rawdata: data, message: msg, hash: sighash}, metaData(ctx), ch) @@ -306,7 +344,8 @@ func (api *SignerAPI) EcRecover(ctx context.Context, data, sig hexutil.Bytes) (c } sig[64] -= 27 // Transform yellow paper V from 27/28 to 0/1 - rpk, err := crypto.Ecrecover(signHash(data), sig) + hash, _ := signHash(data) + rpk, err := crypto.Ecrecover(hash, sig) if err != nil { return common.Address{}, err } @@ -322,9 +361,9 @@ func (api *SignerAPI) EcRecover(ctx context.Context, data, sig hexutil.Bytes) (c // 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 { +func signHash(data []byte) ([]byte, string) { msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data) - return crypto.Keccak256([]byte(msg)) + return crypto.Keccak256([]byte(msg)), msg } // Export returns encrypted private key associated with the given address in web3 keystore format. @@ -341,7 +380,7 @@ func (api *SignerAPI) Export(ctx context.Context, addr common.Address) (json.Raw if url.Scheme != keystore.KeyStoreScheme { return nil, fmt.Errorf("account is not a password protected account") } - ch := make(chan ApprovalStatus, 1) + ch := make(chan ExportResponse, 1) api.ui.ApproveExport(&ExportRequest{account: account, file: url.Path}, metaData(ctx), ch) @@ -361,7 +400,7 @@ func (api *SignerAPI) Import(ctx context.Context, keyJSON json.RawMessage, passp return Account{}, errors.New("password based accounts not supported") } - ch := make(chan ApprovalStatus, 1) + ch := make(chan ImportResponse, 1) api.ui.ApproveImport(&ImportRequest{}, metaData(ctx), ch) if resp := <-ch; resp.approved { diff --git a/cmd/signer/cliui.go b/cmd/signer/cliui.go index 35c4356cec..c8c02b5864 100644 --- a/cmd/signer/cliui.go +++ b/cmd/signer/cliui.go @@ -18,31 +18,64 @@ package main import ( "bufio" "fmt" - "github.com/ethereum/go-ethereum/common" "os" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "golang.org/x/crypto/ssh/terminal" ) type CommandlineUI struct { + in *bufio.Reader } func NewCommandlineUI() *CommandlineUI { - return &CommandlineUI{} + return &CommandlineUI{bufio.NewReader(os.Stdin)} } -func confirm() bool { - fmt.Printf("Type 'Yes' to approve\n$>") - scanner := bufio.NewScanner(os.Stdin) - scanner.Scan() - answer := scanner.Text() - if answer == "Yes" { + +// readString reads a single line from stdin, trimming if from spaces, enforcing +// non-emptyness. +func (ui *CommandlineUI) readString() string { + for { + fmt.Printf("> ") + text, err := ui.in.ReadString('\n') + if err != nil { + log.Crit("Failed to read user input", "err", err) + } + if text = strings.TrimSpace(text); text != "" { + return text + } + } +} + +// readPassword reads a single line from stdin, trimming it from the trailing new +// line and returns it. The input will not be echoed. +func (ui *CommandlineUI) readPassword() string { + fmt.Printf("> ") + text, err := terminal.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + log.Crit("Failed to read password", "err", err) + } + fmt.Println() + return string(text) +} + +// confirm returns true if user enters 'Yes', otherwise false +func (ui *CommandlineUI) confirm() bool { + fmt.Printf("Type 'Yes' to approve\n") + if ui.readString() == "Yes" { return true } return false } + func showMetadata(metadata Metadata) { fmt.Printf("Request info: %v -> %v -> %v\n", metadata.remote, metadata.scheme, metadata.local) } -func (ui *CommandlineUI) ApproveTx(request *SignTxRequest, metadata Metadata, ch chan ApprovalStatus) { +// ApproveTx prompt the user for confirmation to request to sign transaction +func (ui *CommandlineUI) ApproveTx(request *SignTxRequest, metadata Metadata, ch chan SignTxResponse) { fmt.Printf("--------- Transaction request-------------\n") fmt.Printf("to: %v\n", request.transaction.To()) @@ -51,9 +84,11 @@ func (ui *CommandlineUI) ApproveTx(request *SignTxRequest, metadata Metadata, ch fmt.Printf("data: %v\n", common.Bytes2Hex(request.transaction.Data())) fmt.Printf("-------------------------------------------\n") showMetadata(metadata) - ch <- ApprovalStatus{common.Hash{}, confirm(), ""} + ch <- SignTxResponse{request.transaction.Hash(), ui.confirm(), ""} } -func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest, metadata Metadata, ch chan ApprovalStatus) { + +// ApproveSignData prompt the user for confirmation to request to sign data +func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest, metadata Metadata, ch chan SignDataResponse) { fmt.Printf("-------- Sign data request--------------\n") fmt.Printf("account: %x\n", request.account.Address) @@ -62,9 +97,11 @@ func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest, metadata Meta fmt.Printf("message hash: %v\n", request.hash) fmt.Printf("-------------------------------------------\n") showMetadata(metadata) - ch <- ApprovalStatus{common.Hash{}, confirm(), ""} + ch <- SignDataResponse{ui.confirm(), ""} } -func (ui *CommandlineUI) ApproveExport(request *ExportRequest, metadata Metadata, ch chan ApprovalStatus) { + +// ApproveExport prompt the user for confirmation to export encrypted account json +func (ui *CommandlineUI) ApproveExport(request *ExportRequest, metadata Metadata, ch chan ExportResponse) { fmt.Printf("-------- Export account request--------------\n") fmt.Printf("A request has been made to export the (encrypted) keyfile\n") fmt.Printf("Approving this operation means that the caller obtains the (encrypted) contents\n") @@ -73,16 +110,21 @@ func (ui *CommandlineUI) ApproveExport(request *ExportRequest, metadata Metadata fmt.Printf("keyfile: \n%v\n", request.file) fmt.Printf("-------------------------------------------\n") showMetadata(metadata) - ch <- ApprovalStatus{common.Hash{}, confirm(), ""} + ch <- ExportResponse{ui.confirm()} } -func (ui *CommandlineUI) ApproveImport(request *ImportRequest, metadata Metadata, ch chan ApprovalStatus) { + +// ApproveImport prompt the user for confirmation to import account json +func (ui *CommandlineUI) ApproveImport(request *ImportRequest, metadata Metadata, ch chan ImportResponse) { fmt.Printf("-------- Export account request--------------\n") fmt.Printf("A request has been made to import an encrypted keyfile\n") fmt.Printf("-------------------------------------------\n") showMetadata(metadata) - ch <- ApprovalStatus{common.Hash{}, confirm(), ""} + ch <- ImportResponse{ui.confirm(), "", ""} } -func (ui *CommandlineUI) ApproveListing(request *ListRequest, metadata Metadata, ch chan ListApproval) { + +// ApproveListing prompt the user for confirmation to list accounts +// the list of accounts to list can be modified by the ui +func (ui *CommandlineUI) ApproveListing(request *ListRequest, metadata Metadata, ch chan ListResponse) { fmt.Printf("-------- List account request--------------\n") fmt.Printf("A request has been made to list all accounts. \n") @@ -92,26 +134,31 @@ func (ui *CommandlineUI) ApproveListing(request *ListRequest, metadata Metadata, } fmt.Printf("-------------------------------------------\n") showMetadata(metadata) - if confirm() { - ch <- ListApproval{request.accounts} + if ui.confirm() { + ch <- ListResponse{request.accounts} } else { - ch <- ListApproval{nil} + ch <- ListResponse{nil} } } -func (ui *CommandlineUI) ApproveNewAccount(requst *NewAccountRequest, metadata Metadata, ch chan bool) { + +// ApproveNewAccount prompt the user for confirmation to create new account, and reveal to caller +func (ui *CommandlineUI) ApproveNewAccount(requst *NewAccountRequest, metadata Metadata, ch chan NewAccountResponse) { fmt.Printf("-------- New account request--------------\n") fmt.Printf("A request has been made to create a new. \n") fmt.Printf("Approving this operation means that a new account is created,\n") fmt.Printf("and the address show to the caller\n") showMetadata(metadata) - ch <- confirm() + ch <- NewAccountResponse{ui.confirm(), ""} } +// ShowError displays error message to user func (ui *CommandlineUI) ShowError(message string) { - //stdout is used by communication + fmt.Printf("ERROR: %v", message) } + +// ShowInfo displays info message to user func (ui *CommandlineUI) ShowInfo(message string) { - //stdout is used by communication + fmt.Printf("Info: %v", message) } diff --git a/cmd/signer/main.go b/cmd/signer/main.go index fba7f927ef..790853d2e3 100644 --- a/cmd/signer/main.go +++ b/cmd/signer/main.go @@ -19,54 +19,79 @@ package main import ( - "flag" "io" "os" "path/filepath" + "fmt" + "net" + "github.com/ethereum/go-ethereum/cmd/utils" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/log" - "net" - "fmt" -) - -var ( - ksLocation = flag.String("keystore", filepath.Join(node.DefaultDataDir(), "keystore"), "Directory for the keystore") - chainID = flag.Int64("chainid", params.MainnetChainConfig.ChainId.Int64(), "chain identifier") + "gopkg.in/urfave/cli.v1" ) func main() { - flag.Parse() - var ( - server = rpc.NewServer() - api = NewSignerAPI(*chainID, *ksLocation, true, NewCommandlineUI()) - listener net.Listener - err error - ) - - // register signer API with server - if err = server.RegisterName("account", api); err != nil { - utils.Fatalf("Could not register signer API: %v", err) + app := cli.NewApp() + app.Name = "signer" + app.Usage = "Manage ethereum account operations" + app.Flags = []cli.Flag{ + cli.Int64Flag{ + Name: "chainid", + Value: params.MainnetChainConfig.ChainId.Int64(), + Usage: "chain identifier", + }, + cli.IntFlag{ + Name: "loglevel", + Value: 4, + Usage: "log level to emit to the screen", + }, + cli.StringFlag{ + Name: "keystore", + Value: filepath.Join(node.DefaultDataDir(), "keystore"), + Usage: "Directory for the keystore", + }, } + app.Action = func(c *cli.Context) error { + // Set up the logger to print everything and the random generator + log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int("loglevel")), log.StreamHandler(os.Stdout, log.TerminalFormat(true)))) - endpoint := "localhost:8550" + var ( + server = rpc.NewServer() + api = NewSignerAPI(c.Int64("chainid"), c.String("keystore"), true, NewCommandlineUI()) + listener net.Listener + err error + ) - if listener, err = net.Listen("tcp", endpoint); err != nil { - utils.Fatalf("Could not start http listener: %v", err) + // register signer API with server + if err = server.RegisterName("account", api); err != nil { + utils.Fatalf("Could not register signer API: %v", err) + } + + endpoint := "localhost:8550" + + if listener, err = net.Listen("tcp", endpoint); err != nil { + utils.Fatalf("Could not start http listener: %v", err) + } + log.Info(fmt.Sprintf("HTTP endpoint opened: http://%s", endpoint)) + cors := []string{"*"} + + rpc.NewHTTPServer(cors, server).Serve(listener) + return nil } - log.Info(fmt.Sprintf("HTTP endpoint opened: http://%s", endpoint)) - fmt.Printf("HTTP endpoint opened: http://%s\n", endpoint) - cors := []string{"*"} + app.Run(os.Args) - rpc.NewHTTPServer(cors, server).Serve(listener) } -// Create account -// #curl -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_new","params":["test"],"id":67}' localhost:8550 +// Create account +// curl -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_new","params":["test"],"id":67}' localhost:8550 + +// List accounts +// curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_list","params":[""],"id":67}' http://localhost:8550/ type rwc struct { io.Reader io.Writer