diff --git a/cmd/signer/api.go b/cmd/signer/api.go index 66c09f1d05..82da94a2cb 100644 --- a/cmd/signer/api.go +++ b/cmd/signer/api.go @@ -17,15 +17,13 @@ package main import ( - "fmt" - - "errors" - "context" - - "math/big" - + "encoding/json" + "errors" + "fmt" "io/ioutil" + "math/big" + "os" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/keystore" @@ -36,12 +34,97 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" - "encoding/json" ) type SignerAPI struct { chainID *big.Int am *accounts.Manager + ui SignerUI +} + +// Metadata about the request +type Metadata struct { + remote string + local string + scheme string +} + +// The SignTxRequest contains info about a transaction tos sign +type SignTxRequest struct { + transaction *types.Transaction + from accounts.Account +} + +type ExportRequest struct { + account accounts.Account + file string +} +type ImportRequest struct { + account accounts.Account +} +type SignDataRequest struct { + account accounts.Account + rawdata hexutil.Bytes + message string + hash hexutil.Bytes +} +type ApprovalStatus struct { + hash common.Hash + approved bool + pw string +} + +type NewAccountRequest struct{} + +type ListRequest struct { + accounts []Account +} +type ListApproval 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 + ShowError(message string) + 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) ApproveSignData(request *SignDataRequest, metadata Metadata, ch chan ApprovalStatus) { + ch <- ApprovalStatus{common.Hash{}, true, ""} +} +func (ui *HeadlessUI) ApproveExport(request *ExportRequest, metadata Metadata, ch chan ApprovalStatus) { + ch <- ApprovalStatus{common.Hash{}, true, ""} +} +func (ui *HeadlessUI) ApproveImport(request *ImportRequest, metadata Metadata, ch chan ApprovalStatus) { + ch <- ApprovalStatus{common.Hash{}, true, ""} +} +func (ui *HeadlessUI) ApproveListing(request *ListRequest, metadata Metadata, ch chan ListApproval) { + ch <- ListApproval{request.accounts} +} +func (ui *HeadlessUI) ApproveNewAccount(requst *NewAccountRequest, metadata Metadata, ch chan bool) { + ch <- true +} +func (ui *HeadlessUI) ShowError(message string) { + //stdout is used by communication + fmt.Fprint(os.Stderr, message) +} +func (ui *HeadlessUI) ShowInfo(message string) { + //stdout is used by communication + fmt.Fprint(os.Stderr, message) } // NewSignerAPI creates a new API that can be used for account management. @@ -49,7 +132,7 @@ type SignerAPI struct { // key that is generated when a new account is created. // noUSB disables USB support that is required to support hardware devices such as // ledger and trezor. -func NewSignerAPI(chainID int64, ksLocation string, noUSB bool) *SignerAPI { +func NewSignerAPI(chainID int64, ksLocation string, noUSB bool, ui SignerUI) *SignerAPI { var backends []accounts.Backend // support password based accounts @@ -74,12 +157,30 @@ func NewSignerAPI(chainID int64, ksLocation string, noUSB bool) *SignerAPI { } } - return &SignerAPI{big.NewInt(chainID), accounts.NewManager(backends...)} + return &SignerAPI{big.NewInt(chainID), accounts.NewManager(backends...), ui} +} + +func metaData(ctx context.Context) Metadata { + m := Metadata{"NA", "NA", "NA"} + + if v := ctx.Value("remote"); v != nil{ + m.remote = v.(string) + } + if v := ctx.Value("scheme"); v != nil{ + m.scheme = v.(string) + } + if v := ctx.Value("local"); v != nil{ + m.local = v.(string) + } + return m } // List returns the set of wallet this signer manages. Each wallet can contain // multiple accounts. -func (api *SignerAPI) List(ctx context.Context) []Account { +func (api *SignerAPI) List(ctx context.Context) ([]Account, error) { + + ch := make(chan ListApproval, 1) + var accounts []Account for _, wallet := range api.am.Wallets() { for _, acc := range wallet.Accounts() { @@ -87,7 +188,12 @@ func (api *SignerAPI) List(ctx context.Context) []Account { accounts = append(accounts, acc) } } - return accounts + + api.ui.ApproveListing(&ListRequest{accounts: accounts}, metaData(ctx), ch) + if result := <-ch; result.accounts != nil { + return result.accounts, nil + } + return nil, fmt.Errorf("Listing denied") } // New creates a new password protected account. The private key is protected with @@ -98,8 +204,13 @@ 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") } - acc, err := be[0].(*keystore.KeyStore).NewAccount(passphrase) - return acc, err + ch := make(chan bool, 1) + api.ui.ApproveNewAccount(&NewAccountRequest{}, metaData(ctx), ch) + + if <-ch { + return be[0].(*keystore.KeyStore).NewAccount(passphrase) + } + return accounts.Account{}, fmt.Errorf("Request denied") } // SignTransaction signs the given transaction and returns it in an RLP encoded form @@ -119,12 +230,23 @@ 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) } - signedTx, err := wallet.SignTxWithPassphrase(acc, passwd, tx, api.chainID) - if err != nil { - return nil, err + ch := make(chan ApprovalStatus, 1) + api.ui.ApproveTx(&SignTxRequest{transaction: tx, from: acc}, metaData(ctx), ch) + + if result := <-ch; result.approved { + //Sanity check + if result.hash != tx.Hash() { + return nil, fmt.Errorf("Transaction hash mismatch") + } + signedTx, err := wallet.SignTxWithPassphrase(acc, passwd, tx, api.chainID) + if err != nil { + return nil, err + } + return rlp.EncodeToBytes(signedTx) } - return rlp.EncodeToBytes(signedTx) + return nil, fmt.Errorf("Transaction rejected") + } // Sign calculates an Ethereum ECDSA signature for: @@ -144,13 +266,25 @@ func (api *SignerAPI) Sign(ctx context.Context, addr common.Address, passwd stri if err != nil { return nil, err } - // Assemble sign the data with the wallet - signature, err := wallet.SignHashWithPassphrase(account, passwd, signHash(data)) - if err != nil { - return nil, err + ch := make(chan ApprovalStatus, 1) + + msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data) + sighash := crypto.Keccak256([]byte(msg)) + + api.ui.ApproveSignData(&SignDataRequest{account: account, rawdata: data, message: msg, hash: sighash}, metaData(ctx), ch) + + if (<-ch).approved { + + // Assemble sign the data with the wallet + signature, err := wallet.SignHashWithPassphrase(account, passwd, sighash) + if err != nil { + return nil, err + } + signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper + return signature, nil + } - signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper - return signature, nil + return nil, fmt.Errorf("Signing rejected") } // EcRecover returns the address for the account that was used to create the signature. @@ -207,8 +341,14 @@ 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) - return ioutil.ReadFile(url.Path) + api.ui.ApproveExport(&ExportRequest{account: account, file: url.Path}, metaData(ctx), ch) + + if (<-ch).approved { + return ioutil.ReadFile(url.Path) + } + return nil, fmt.Errorf("Export rejected") } // Imports tries to import the given keyJSON in the local keystore. The keyJSON data is expected to be @@ -216,14 +356,23 @@ func (api *SignerAPI) Export(ctx context.Context, addr common.Address) (json.Raw // decryption it will encrypt the key with the given newPassphrase and store it in the keystore. func (api *SignerAPI) Import(ctx context.Context, keyJSON json.RawMessage, passphrase, newPassphrase string) (Account, error) { be := api.am.Backends(keystore.KeyStoreType) + if len(be) == 0 { return Account{}, errors.New("password based accounts not supported") } - acc, err := be[0].(*keystore.KeyStore).Import(keyJSON, passphrase, newPassphrase) - if err != nil { - return Account{}, err - } + ch := make(chan ApprovalStatus, 1) - return Account{Typ: "account", URL: acc.URL, Address: acc.Address}, nil -} \ No newline at end of file + api.ui.ApproveImport(&ImportRequest{}, metaData(ctx), ch) + if resp := <-ch; resp.approved { + + acc, err := be[0].(*keystore.KeyStore).Import(keyJSON, passphrase, newPassphrase) + if err != nil { + return Account{}, err + } + + return Account{Typ: "account", URL: acc.URL, Address: acc.Address}, nil + } + return Account{}, fmt.Errorf("Import rejected") + +} diff --git a/cmd/signer/cliui.go b/cmd/signer/cliui.go new file mode 100644 index 0000000000..35c4356cec --- /dev/null +++ b/cmd/signer/cliui.go @@ -0,0 +1,117 @@ +// 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 ( + "bufio" + "fmt" + "github.com/ethereum/go-ethereum/common" + "os" +) + +type CommandlineUI struct { +} + +func NewCommandlineUI() *CommandlineUI { + return &CommandlineUI{} +} +func confirm() bool { + fmt.Printf("Type 'Yes' to approve\n$>") + scanner := bufio.NewScanner(os.Stdin) + scanner.Scan() + answer := scanner.Text() + if answer == "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) { + + fmt.Printf("--------- Transaction request-------------\n") + fmt.Printf("to: %v\n", request.transaction.To()) + fmt.Printf("from: %v\n", request.from) + fmt.Printf("value: %v\n", request.transaction.Value()) + fmt.Printf("data: %v\n", common.Bytes2Hex(request.transaction.Data())) + fmt.Printf("-------------------------------------------\n") + showMetadata(metadata) + ch <- ApprovalStatus{common.Hash{}, confirm(), ""} +} +func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest, metadata Metadata, ch chan ApprovalStatus) { + + fmt.Printf("-------- Sign data request--------------\n") + fmt.Printf("account: %x\n", request.account.Address) + fmt.Printf("message: \n%v\n", request.message) + fmt.Printf("raw data: \n%v\n", request.rawdata) + fmt.Printf("message hash: %v\n", request.hash) + fmt.Printf("-------------------------------------------\n") + showMetadata(metadata) + ch <- ApprovalStatus{common.Hash{}, confirm(), ""} +} +func (ui *CommandlineUI) ApproveExport(request *ExportRequest, metadata Metadata, ch chan ApprovalStatus) { + 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") + fmt.Printf("\n") + fmt.Printf("account: %x\n", request.account.Address) + fmt.Printf("keyfile: \n%v\n", request.file) + fmt.Printf("-------------------------------------------\n") + showMetadata(metadata) + ch <- ApprovalStatus{common.Hash{}, confirm(), ""} +} +func (ui *CommandlineUI) ApproveImport(request *ImportRequest, metadata Metadata, ch chan ApprovalStatus) { + 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(), ""} +} +func (ui *CommandlineUI) ApproveListing(request *ListRequest, metadata Metadata, ch chan ListApproval) { + + fmt.Printf("-------- List account request--------------\n") + fmt.Printf("A request has been made to list all accounts. \n") + fmt.Printf("You can select which accounts the caller can see\n") + for _, account := range request.accounts { + fmt.Printf("\t[x] %v\n", account.Address.Hex()) + } + fmt.Printf("-------------------------------------------\n") + showMetadata(metadata) + if confirm() { + ch <- ListApproval{request.accounts} + } else { + ch <- ListApproval{nil} + } +} +func (ui *CommandlineUI) ApproveNewAccount(requst *NewAccountRequest, metadata Metadata, ch chan bool) { + 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() +} + +func (ui *CommandlineUI) ShowError(message string) { + //stdout is used by communication + fmt.Printf("ERROR: %v", message) +} +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 803e9e8ef5..fba7f927ef 100644 --- a/cmd/signer/main.go +++ b/cmd/signer/main.go @@ -19,7 +19,6 @@ package main import ( - "bufio" "flag" "io" "os" @@ -29,6 +28,9 @@ import ( "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 ( @@ -41,19 +43,29 @@ func main() { var ( server = rpc.NewServer() - api = NewSignerAPI(*chainID, *ksLocation, true) + api = NewSignerAPI(*chainID, *ksLocation, true, NewCommandlineUI()) + listener net.Listener + err error ) // register signer API with server - if err := server.RegisterName("account", api); err != nil { + if err = server.RegisterName("account", api); err != nil { utils.Fatalf("Could not register signer API: %v", err) } - // start server with in-/output connected to stdin/stdout - in, out := bufio.NewReader(os.Stdin), os.Stdout - codec := rpc.NewJSONCodec(&rwc{in, out}) - server.ServeCodec(codec, rpc.OptionMethodInvocation|rpc.OptionSubscriptions) + 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)) + fmt.Printf("HTTP endpoint opened: http://%s\n", endpoint) + cors := []string{"*"} + + 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 type rwc struct { io.Reader diff --git a/rpc/http.go b/rpc/http.go index e8f51150f4..14b6c1ab42 100644 --- a/rpc/http.go +++ b/rpc/http.go @@ -169,12 +169,17 @@ func (srv *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // All checks passed, create a codec that reads direct from the request body // untilEOF and writes the response to w and order the server to process a // single request. + ctx := context.Background() + ctx = context.WithValue(ctx, "remote", r.RemoteAddr) + ctx = context.WithValue(ctx, "scheme", r.Proto) + ctx = context.WithValue(ctx, "local", r.Host) + body := io.LimitReader(r.Body, maxRequestContentLength) codec := NewJSONCodec(&httpReadWriteNopCloser{body, w}) defer codec.Close() w.Header().Set("content-type", contentType) - srv.ServeSingleRequest(codec, OptionMethodInvocation) + srv.ServeSingleRequest(codec, OptionMethodInvocation, ctx) } // validateRequest returns a non-zero response code and error message if the diff --git a/rpc/server.go b/rpc/server.go index 11373b504c..88ad080a8f 100644 --- a/rpc/server.go +++ b/rpc/server.go @@ -125,7 +125,7 @@ func (s *Server) RegisterName(name string, rcvr interface{}) error { // If singleShot is true it will process a single request, otherwise it will handle // requests until the codec returns an error when reading a request (in most cases // an EOF). It executes requests in parallel when singleShot is false. -func (s *Server) serveRequest(codec ServerCodec, singleShot bool, options CodecOption) error { +func (s *Server) serveRequest(codec ServerCodec, singleShot bool, options CodecOption, ctx context.Context) error { var pend sync.WaitGroup defer func() { @@ -140,7 +140,8 @@ func (s *Server) serveRequest(codec ServerCodec, singleShot bool, options CodecO s.codecsMu.Unlock() }() - ctx, cancel := context.WithCancel(context.Background()) +// ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(ctx) defer cancel() // if the codec supports notification include a notifier that callbacks can use @@ -215,14 +216,14 @@ func (s *Server) serveRequest(codec ServerCodec, singleShot bool, options CodecO // stopped. In either case the codec is closed. func (s *Server) ServeCodec(codec ServerCodec, options CodecOption) { defer codec.Close() - s.serveRequest(codec, false, options) + s.serveRequest(codec, false, options,context.Background()) } // ServeSingleRequest reads and processes a single RPC request from the given codec. It will not // close the codec unless a non-recoverable error has occurred. Note, this method will return after // a single request has been processed! -func (s *Server) ServeSingleRequest(codec ServerCodec, options CodecOption) { - s.serveRequest(codec, true, options) +func (s *Server) ServeSingleRequest(codec ServerCodec, options CodecOption, ctx context.Context) { + s.serveRequest(codec, true, options, ctx) } // Stop will stop reading new requests, wait for stopPendingRequestTimeout to allow pending requests to finish,