diff --git a/cmd/signer/README.md b/cmd/signer/README.md index 513e08dbc1..bc4f131040 100644 --- a/cmd/signer/README.md +++ b/cmd/signer/README.md @@ -25,6 +25,7 @@ The signer accepts the following command line options: --rpcaddr value HTTP-RPC server listening interface (default: "localhost") --rpcport value HTTP-RPC server listening port (default: 8550) --4bytedb value File containing 4byte-identifiers (default: "./4byte.json") + --stdio-ui Use STDIN/STDOUT as a channel for an external UI. This means that an STDIN/STDOUT is used for RPC-communication with a e.g. a graphical user interface, and can be used when the signer is started by an external process. --help, -h show help ``` @@ -34,15 +35,39 @@ Example: signer -keystore /my/keystore -chainid 4 ``` -## Communicating +## Communication -The signer listens to HTTP requests on `rpcaddr`:`rpcport`. The messages are expected to be JSON -[jsonrpc 2.0 standard](http://www.jsonrpc.org/specification). +### External API -Some of these call can require user interaction. Clients must be aware that responses may be deplayed significanlty or -may never be received if a users decideds to ignore the confirmation request. +The signer listens to HTTP requests on `rpcaddr`:`rpcport`. The messages are +expected to be JSON [jsonrpc 2.0 standard](http://www.jsonrpc.org/specification). -## API +Some of these call can require user interaction. Clients must be aware that responses +may be deplayed significanlty or may never be received if a users decideds to ignore the confirmation request. + +The External API is **untrusted** : it does not accept credentials over this api, nor does it expect +that requests have any authority. + +### UI API + +The signer has one native console-based UI, for operation without any standalone tools. +However, there is also an API to communicate with an external UI. To enable that UI, +the signer needs to be executed with the `--stdio-ui` option, which allocates the +`stdin`/`stdout` for the UI-api. + +An example (insecure) proof-of-concept of has been implemented in `pythonsigner.py`. + +The model is as follows: + +* The user starts the UI app (`pythonsigner.py`). +* The UI app starts the `signer` with `--stdio-ui`, and listens to the +process output for confirmation-requests. +* The `signer` opens the external http api. +* When the `signer` receives requests, it sends a `jsonrpc` request via `stdout`. +* The UI app prompts the user accordingly, and responds to the `signer` +* The `signer` signs (or not), and responds to the original request. + +## External API ### Encoding - number: positive integers that are hex encoded @@ -348,3 +373,8 @@ None +## UI API + +These methods needs to be implemented by a UI listener. + +still work in progress \ No newline at end of file diff --git a/cmd/signer/api.go b/cmd/signer/api.go index b14a8e4faf..8606eb1e38 100644 --- a/cmd/signer/api.go +++ b/cmd/signer/api.go @@ -24,6 +24,7 @@ import ( "io/ioutil" "math/big" + "bytes" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/accounts/usbwallet" @@ -51,57 +52,63 @@ type Metadata struct { // types for the requests/response types type ( - // SignTxRequest contains info about a transaction to sign + // SignTxRequest contains info about a Transaction to sign SignTxRequest struct { - transaction types.Transaction - from accounts.Account - callinfo fmt.Stringer + Transaction TransactionArg + From common.Address + Callinfo fmt.Stringer + Meta Metadata } // SignTxResponse result from SignTxRequest SignTxResponse struct { //The UI may make changes to the TX - transaction types.Transaction - approved bool - pw string + Transaction TransactionArg + From common.Address + Approved bool + Password string } // ExportRequest info about query to export accounts ExportRequest struct { - account accounts.Account - file string + Address common.Address + Meta Metadata } // ExportResponse response to export-request ExportResponse struct { - approved bool + Approved bool } - // ImportRequest info about request to import an account + // ImportRequest info about request to import an Account ImportRequest struct { - account accounts.Account + Meta Metadata } ImportResponse struct { - approved bool - oldPassword string - newPassword string + Approved bool + OldPassword string + NewPassword string } SignDataRequest struct { - account accounts.Account - rawdata hexutil.Bytes - message string - hash hexutil.Bytes + Address common.Address + Rawdata hexutil.Bytes + Message string + Hash hexutil.Bytes + Meta Metadata } SignDataResponse struct { - approved bool - pw string + Approved bool + Password string + } + NewAccountRequest struct { + Meta Metadata } - NewAccountRequest struct{} NewAccountResponse struct { - approved bool - pw string + Approved bool + Password string } ListRequest struct { - accounts []Account + Accounts []Account + Meta Metadata } ListResponse struct { - accounts []Account + Accounts []Account } ) @@ -120,28 +127,28 @@ func (ew errorWrapper) String() string { // for the signer type SignerUI interface { - // ApproveTx prompt the user for confirmation to request to sign transaction - ApproveTx(request *SignTxRequest, metadata Metadata, ch chan SignTxResponse) + // ApproveTx prompt the user for confirmation to request to sign Transaction + ApproveTx(request *SignTxRequest) (SignTxResponse, error) // 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) + ApproveSignData(request *SignDataRequest) (SignDataResponse, error) + // ApproveExport prompt the user for confirmation to export encrypted Account json + ApproveExport(request *ExportRequest) (ExportResponse, error) + // ApproveImport prompt the user for confirmation to import Account json + ApproveImport(request *ImportRequest) (ImportResponse, error) // 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) + ApproveListing(request *ListRequest) (ListResponse, error) + // ApproveNewAccount prompt the user for confirmation to create new Account, and reveal to caller + ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) // ShowError displays error message to user ShowError(message string) // ShowInfo displays info message to user ShowInfo(message string) } -// NewSignerAPI creates a new API that can be used for account management. +// NewSignerAPI creates a new API that can be used for Account management. // ksLocation specifies the directory where to store the password protected private -// key that is generated when a new account is created. +// 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, ui SignerUI, abidb *abiDb, lightKDF bool) *SignerAPI { @@ -194,23 +201,26 @@ func metaData(ctx context.Context) Metadata { // multiple accounts. func (api *SignerAPI) List(ctx context.Context) (Accounts, error) { - ch := make(chan ListResponse, 1) var accs []Account for _, wallet := range api.am.Wallets() { for _, acc := range wallet.Accounts() { - acc := Account{Typ: "account", URL: wallet.URL(), Address: acc.Address} + acc := Account{Typ: "Account", URL: wallet.URL(), Address: acc.Address} accs = append(accs, acc) } } - api.ui.ApproveListing(&ListRequest{accounts: accs}, metaData(ctx), ch) - if result := <-ch; result.accounts != nil { - return result.accounts, nil + result, err := api.ui.ApproveListing(&ListRequest{Accounts: accs, Meta: metaData(ctx)}) + if err != nil { + return nil, err } - return nil, ErrRequestDenied + if result.Accounts == nil { + return nil, ErrRequestDenied + + } + return result.Accounts, nil } -// New creates a new password protected account. The private key is protected with +// New creates a new password protected Account. The private key is protected with // the given password. Users are responsible to backup the private key that is stored // in the keystore location thas was specified when this API was created. func (api *SignerAPI) New(ctx context.Context) (accounts.Account, error) { @@ -218,72 +228,128 @@ func (api *SignerAPI) New(ctx context.Context) (accounts.Account, error) { if len(be) == 0 { return accounts.Account{}, errors.New("password based accounts not supported") } - ch := make(chan NewAccountResponse, 1) - api.ui.ApproveNewAccount(&NewAccountRequest{}, metaData(ctx), ch) - - if resp := <-ch; resp.approved { - return be[0].(*keystore.KeyStore).NewAccount(resp.pw) + resp, err := api.ui.ApproveNewAccount(&NewAccountRequest{metaData(ctx)}) + if err != nil { + return accounts.Account{}, err } - return accounts.Account{}, ErrRequestDenied + if !resp.Approved { + return accounts.Account{}, ErrRequestDenied + } + return be[0].(*keystore.KeyStore).NewAccount(resp.Password) } -// SignTransaction signs the given transaction and returns it in an RLP encoded form +func toTransaction(args *TransactionArg) *types.Transaction { + if args.To == nil { + return types.NewContractCreation(uint64(*args.Nonce), (*big.Int)(args.Value), (*big.Int)(args.Gas), (*big.Int)(args.GasPrice), args.Data) + } else { + return types.NewTransaction(uint64(*args.Nonce), *args.To, (*big.Int)(args.Value), (*big.Int)(args.Gas), (*big.Int)(args.GasPrice), args.Data) + } +} + +// logDiff logs the difference between the incoming (original) transaction and the one returned from the signer. +// it also returns 'true' if the transaction was modified, to make it possible to configure the signer not to allow +// UI-modifications to requests +func logDiff(original *SignTxRequest, new *SignTxResponse) bool { + modified := false + if f0, f1 := original.From, new.From; f0 != f1 { + modified = true + log.Info("Sender-account changed by UI", "was", f0, "is", f1) + } + if t0, t1 := original.Transaction.To, new.Transaction.To; t0 != t1 { + if t0 == nil || t1 == nil || !bytes.Equal(t0.Bytes(), t1.Bytes()) { + log.Info("Recipient-account changed by UI", "was", t0, "is", t1) + modified = true + } + } + if g0, g1 := (*big.Int)(original.Transaction.Gas), (*big.Int)(new.Transaction.Gas); g0 != g1 { + if g0 == nil || g1 == nil || g0.Cmp(g1) != 0 { + modified = true + log.Info("Gas changed by UI", "was", g0, "is", g1) + } + } + if g0, g1 := (*big.Int)(original.Transaction.GasPrice), (*big.Int)(new.Transaction.GasPrice); g0 != g1 { + if g0 == nil || g1 == nil || g0.Cmp(g1) != 0 { + modified = true + log.Info("GasPrice changed by UI", "was", g0, "is", g1) + } + } + if v0, v1 := (*big.Int)(original.Transaction.Value), (*big.Int)(new.Transaction.Value); v0 != v1 { + if v0 == nil || v1 == nil || v0.Cmp(v1) != 0 { + modified = true + log.Info("Value changed by UI", "was", v0, "is", v1) + } + } + if d0, d1 := original.Transaction.Data, new.Transaction.Data; !bytes.Equal(d0, d1) { + modified = true + log.Info("Data changed by UI", "was", common.ToHex(d0), "is", common.ToHex(d1)) + } + if n0, n1 := original.Transaction.Nonce, new.Transaction.Nonce; n0 != n1 { + + if n0 == nil || n1 == nil || (*n0) != (*n1) { + modified = true + log.Info("Nonce changed by UI", "was", n0, "is", n1) + } + } + return modified +} + +// SignTransaction signs the given Transaction and returns it in an RLP encoded form // that can be posted to `eth_sendRawTransaction`. func (api *SignerAPI) SignTransaction(ctx context.Context, from common.Address, args TransactionArg, methodSig *string) (hexutil.Bytes, error) { - acc := accounts.Account{Address: from} - wallet, err := api.am.Find(acc) - if err != nil { - return nil, err - } + var ( + err error + result SignTxResponse + ) - var tx *types.Transaction - if args.To == nil { - tx = types.NewContractCreation(uint64(*args.Nonce), (*big.Int)(args.Value), (*big.Int)(args.Gas), (*big.Int)(args.GasPrice), args.Data) - } else { - tx = types.NewTransaction(uint64(*args.Nonce), *args.To, (*big.Int)(args.Value), (*big.Int)(args.Gas), (*big.Int)(args.GasPrice), args.Data) - } + req := SignTxRequest{Transaction: args, From: from, Meta: metaData(ctx)} - req := SignTxRequest{transaction: *tx, from: acc} - if len(tx.Data()) > 3 { + data := args.Data + if len(data) > 3 { // Try to make sense of the data var abidata string if methodSig == nil { - abidata, err = api.abidb.LookupABI(tx.Data()[:4]) + abidata, err = api.abidb.LookupABI(data[:4]) if err != nil { - req.callinfo = errorWrapper{"Warning! Could not locate ABI", err} + req.Callinfo = errorWrapper{"Warning! Could not locate ABI", err} } } else { abidata = *methodSig } if abidata != "" { - req.callinfo, err = parseCallData(tx.Data(), abidata) + req.Callinfo, err = parseCallData(data, abidata) if err != nil { - req.callinfo = errorWrapper{"Warning! Could not validate ABI-data against calldata", err} + req.Callinfo = errorWrapper{"Warning! Could not validate ABI-data against calldata", err} } } } - ch := make(chan SignTxResponse, 1) - api.ui.ApproveTx(&req, metaData(ctx), ch) + result, err = api.ui.ApproveTx(&req) + if err != nil { + return nil, err + } + if !result.Approved { + return nil, ErrRequestDenied + } + // Log changes made by the UI to the signing-request + logDiff(&req, &result) - if result := <-ch; result.approved { - //Sanity check - if result.transaction.Hash() != tx.Hash() { - api.ui.ShowInfo("Transaction modified by UI") - } - // The one to sign is the one that was returned from the UI - signedTx, err := wallet.SignTxWithPassphrase(acc, result.pw, &result.transaction, api.chainID) - if err != nil { - api.ui.ShowError(err.Error()) - return nil, err - } - return rlp.EncodeToBytes(signedTx) + acc := accounts.Account{Address: result.From} + wallet, err := api.am.Find(acc) + if err != nil { + return nil, err } - return nil, ErrRequestDenied + var tx = toTransaction(&result.Transaction) + // The one to sign is the one that was returned from the UI + signedTx, err := wallet.SignTxWithPassphrase(acc, result.Password, tx, api.chainID) + if err != nil { + api.ui.ShowError(err.Error()) + return nil, err + } + return rlp.EncodeToBytes(signedTx) } // Sign calculates an Ethereum ECDSA signature for: @@ -296,35 +362,39 @@ func (api *SignerAPI) SignTransaction(ctx context.Context, from common.Address, // // https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign func (api *SignerAPI) Sign(ctx context.Context, addr common.Address, data hexutil.Bytes) (hexutil.Bytes, error) { + + sighash, msg := signHash(data) + + // We make the request prior to looking up if we actually have the account, to prevent + // account-enumeration via the API + req := &SignDataRequest{Address: addr, Rawdata: data, Message: msg, Hash: sighash, Meta: metaData(ctx)} + res, err := api.ui.ApproveSignData(req) + + if err != nil { + return nil, err + } + if !res.Approved { + return nil, ErrRequestDenied + } + // Look up the wallet containing the requested signer account := accounts.Account{Address: addr} - wallet, err := api.am.Find(account) if err != nil { return nil, err } - ch := make(chan SignDataResponse, 1) - - sighash, msg := signHash(data) - - api.ui.ApproveSignData(&SignDataRequest{account: account, rawdata: data, message: msg, hash: sighash}, metaData(ctx), ch) - - if res := <-ch; res.approved { - - // Assemble sign the data with the wallet - signature, err := wallet.SignHashWithPassphrase(account, res.pw, sighash) - if err != nil { - api.ui.ShowError(err.Error()) - return nil, err - } - signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper - return signature, nil - + // Assemble sign the data with the wallet + signature, err := wallet.SignHashWithPassphrase(account, res.Password, sighash) + if err != nil { + api.ui.ShowError(err.Error()) + return nil, err } - return nil, ErrRequestDenied + signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper + return signature, nil + } -// EcRecover returns the address for the account that was used to create the signature. +// EcRecover returns the address for the Account that was used to create the signature. // Note, this function is compatible with eth_sign and personal_sign. As such it recovers // the address of: // hash = keccak256("\x19Ethereum Signed Message:\n"${message length}${message}) @@ -367,26 +437,27 @@ func signHash(data []byte) ([]byte, string) { // Export returns encrypted private key associated with the given address in web3 keystore format. func (api *SignerAPI) Export(ctx context.Context, addr common.Address) (json.RawMessage, error) { - // Look up the wallet containing the requested signer - account := accounts.Account{Address: addr} - wallet, err := api.am.Find(account) + res, err := api.ui.ApproveExport(&ExportRequest{Address: addr, Meta: metaData(ctx)}) + + if err != nil { + return nil, err + } + if !res.Approved { + return nil, ErrRequestDenied + } + + // Look up the wallet containing the requested signer + wallet, err := api.am.Find(accounts.Account{Address: addr}) if err != nil { return nil, err } - url := wallet.URL() - if url.Scheme != keystore.KeyStoreScheme { - return nil, fmt.Errorf("account is not a password protected account") + if wallet.URL().Scheme != keystore.KeyStoreScheme { + return nil, fmt.Errorf("Account is not a keystore-account") } - ch := make(chan ExportResponse, 1) - api.ui.ApproveExport(&ExportRequest{account: account, file: url.Path}, metaData(ctx), ch) - - if (<-ch).approved { - return ioutil.ReadFile(url.Path) - } - return nil, ErrRequestDenied + return ioutil.ReadFile(wallet.URL().Path) } // Imports tries to import the given keyJSON in the local keystore. The keyJSON data is expected to be @@ -400,20 +471,21 @@ func (api *SignerAPI) Import(ctx context.Context, string, keyJSON json.RawMessag return Account{}, errors.New("password based accounts not supported") } - ch := make(chan ImportResponse, 1) + res, err := api.ui.ApproveImport(&ImportRequest{Meta: metaData(ctx)}) - api.ui.ApproveImport(&ImportRequest{}, metaData(ctx), ch) - - if resp := <-ch; resp.approved { - - acc, err := be[0].(*keystore.KeyStore).Import(keyJSON, resp.oldPassword, resp.newPassword) - if err != nil { - api.ui.ShowError(err.Error()) - return Account{}, err - } - - return Account{Typ: "account", URL: acc.URL, Address: acc.Address}, nil + if err != nil { + return Account{}, err } - return Account{}, ErrRequestDenied + if !res.Approved { + return Account{}, ErrRequestDenied + } + + acc, err := be[0].(*keystore.KeyStore).Import(keyJSON, res.OldPassword, res.NewPassword) + if err != nil { + api.ui.ShowError(err.Error()) + return Account{}, err + } + + return Account{Typ: "Account", URL: acc.URL, Address: acc.Address}, nil } diff --git a/cmd/signer/api_test.go b/cmd/signer/api_test.go index fa553a2af6..412bc355d2 100644 --- a/cmd/signer/api_test.go +++ b/cmd/signer/api_test.go @@ -8,7 +8,6 @@ import ( "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/types" "io/ioutil" "math/big" "os" @@ -22,70 +21,57 @@ type HeadlessUI struct { controller chan string } -func (ui *HeadlessUI) ApproveTx(request *SignTxRequest, metadata Metadata, ch chan SignTxResponse) { +func (ui *HeadlessUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) { switch <-ui.controller { case "Y": - ch <- SignTxResponse{request.transaction, true, <-ui.controller} + return SignTxResponse{request.Transaction, request.From, true, <-ui.controller}, nil case "M": //Modify - old := request.transaction - newVal := big.NewInt(0).Add(old.Value(), big.NewInt(1)) - tx := types.NewTransaction(old.Nonce(), *old.To(), newVal, old.Gas(), old.GasPrice(), old.Data()) - ch <- SignTxResponse{*tx, true, <-ui.controller} + old := (*big.Int)(request.Transaction.Value) + newVal := big.NewInt(0).Add(old, big.NewInt(1)) + request.Transaction.Value = (*hexutil.Big)(newVal) + return SignTxResponse{request.Transaction, request.From, true, <-ui.controller}, nil default: - ch <- SignTxResponse{request.transaction, false, ""} + return SignTxResponse{request.Transaction, request.From, false, ""}, nil } } -func (ui *HeadlessUI) ApproveSignData(request *SignDataRequest, metadata Metadata, ch chan SignDataResponse) { - switch <-ui.controller { - case "Y": - ch <- SignDataResponse{true, <-ui.controller} - default: - ch <- SignDataResponse{false, ""} +func (ui *HeadlessUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) { + if "Y" == <-ui.controller { + return SignDataResponse{true, <-ui.controller}, nil } + return SignDataResponse{false, ""}, nil } -func (ui *HeadlessUI) ApproveExport(request *ExportRequest, metadata Metadata, ch chan ExportResponse) { +func (ui *HeadlessUI) ApproveExport(request *ExportRequest) (ExportResponse, error) { - switch <-ui.controller { - case "Y": - ch <- ExportResponse{true} - default: - ch <- ExportResponse{false} - } + return ExportResponse{<-ui.controller == "Y"}, nil } -func (ui *HeadlessUI) ApproveImport(request *ImportRequest, metadata Metadata, ch chan ImportResponse) { +func (ui *HeadlessUI) ApproveImport(request *ImportRequest) (ImportResponse, error) { - switch <-ui.controller { - case "Y": - ch <- ImportResponse{true, <-ui.controller, <-ui.controller} - default: - ch <- ImportResponse{false, "", ""} + if "Y" == <-ui.controller { + return ImportResponse{true, <-ui.controller, <-ui.controller}, nil } - + return ImportResponse{false, "", ""}, nil } -func (ui *HeadlessUI) ApproveListing(request *ListRequest, metadata Metadata, ch chan ListResponse) { +func (ui *HeadlessUI) ApproveListing(request *ListRequest) (ListResponse, error) { switch <-ui.controller { case "A": - ch <- ListResponse{request.accounts} + return ListResponse{request.Accounts}, nil case "1": l := make([]Account, 1) - l[0] = request.accounts[1] - ch <- ListResponse{l} + l[0] = request.Accounts[1] + return ListResponse{l}, nil default: - ch <- ListResponse{nil} + return ListResponse{nil}, nil } - } -func (ui *HeadlessUI) ApproveNewAccount(requst *NewAccountRequest, metadata Metadata, ch chan NewAccountResponse) { +func (ui *HeadlessUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) { - switch <-ui.controller { - case "Y": - ch <- NewAccountResponse{true, <-ui.controller} - default: - ch <- NewAccountResponse{false, ""} + if "Y" == <-ui.controller { + return NewAccountResponse{true, <-ui.controller}, nil } + return NewAccountResponse{false, ""}, nil } func (ui *HeadlessUI) ShowError(message string) { //stdout is used by communication @@ -179,14 +165,14 @@ func TestNewAcc(t *testing.T) { verifyNum(4) // Testing listing: - // Listing one account + // Listing one Account control <- "1" list, err := api.List(context.Background()) if err != nil { t.Fatal(err) } if len(list) != 1 { - t.Fatalf("List should only show one account") + t.Fatalf("List should only show one Account") } // Listing denied control <- "Nope" @@ -333,3 +319,31 @@ func TestSignTx(t *testing.T) { } } + +/* +func TestAsyncronousResponses(t *testing.T){ + + //Set up one account + api, control := setup(t) + createAccount(control, api, t) + + // Two transactions, the second one with larger value than the first + tx1 := mkTestTx() + newVal := big.NewInt(0).Add((*big.Int) (tx1.Value), big.NewInt(1)) + tx2 := mkTestTx() + tx2.Value = (*hexutil.Big)(newVal) + + control <- "W" //wait + control <- "Y" // + control <- "apassword" + control <- "Y" // + control <- "apassword" + + var err error + + h1, err := api.SignTransaction(context.Background(), common.HexToAddress("1111"), tx1, nil) + h2, err := api.SignTransaction(context.Background(), common.HexToAddress("2222"), tx2, nil) + + + } +*/ diff --git a/cmd/signer/cliui.go b/cmd/signer/cliui.go index 3898bac917..f2b1d6f9df 100644 --- a/cmd/signer/cliui.go +++ b/cmd/signer/cliui.go @@ -56,13 +56,11 @@ func (ui *CommandlineUI) readString() string { func (ui *CommandlineUI) readPassword() string { fmt.Printf("Enter password to approve:\n") fmt.Printf("> ") - //TODO; remove this, only for debuggging within IDE - text := "foobar" - //TODO: Use this - // text, err := terminal.ReadPassword(int(os.Stdin.Fd())) - //if err != nil { - // log.Crit("Failed to read password", "err", err) - //} + + text, err := terminal.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + log.Crit("Failed to read password", "err", err) + } fmt.Println() fmt.Println("-----------------------") return string(text) @@ -95,112 +93,110 @@ func showMetadata(metadata Metadata) { fmt.Printf("Request info:\n\t%v -> %v -> %v\n", metadata.remote, metadata.scheme, metadata.local) } -// ApproveTx prompt the user for confirmation to request to sign transaction -func (ui *CommandlineUI) ApproveTx(request *SignTxRequest, metadata Metadata, ch chan SignTxResponse) { +// ApproveTx prompt the user for confirmation to request to sign Transaction +func (ui *CommandlineUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) { ui.mu.Lock() defer ui.mu.Unlock() - weival := request.transaction.Value() + weival := request.Transaction.Value fmt.Printf("--------- Transaction request-------------\n") - fmt.Printf("to: %v\n", request.transaction.To().Hex()) - fmt.Printf("from: %v\n", request.from.Address.Hex()) + fmt.Printf("to: %v\n", request.Transaction.To.Hex()) + fmt.Printf("from: %v\n", request.From.Hex()) fmt.Printf("value: %v wei\n", weival) - if len(request.transaction.Data()) > 0 { - fmt.Printf("data: %v\n", common.Bytes2Hex(request.transaction.Data())) + if len(request.Transaction.Data) > 0 { + fmt.Printf("data: %v\n", common.Bytes2Hex(request.Transaction.Data)) } - if request.callinfo != nil { - fmt.Printf("\nNote: This transaction contains data. Review abi-decoding info below:") - fmt.Printf("\nCall info:\n\t%v\n", request.callinfo.String()) + if request.Callinfo != nil { + fmt.Printf("\nNote: This Transaction contains data. Review abi-decoding info below:") + fmt.Printf("\nCall info:\n\t%v\n", request.Callinfo.String()) } fmt.Printf("\n") - showMetadata(metadata) + showMetadata(request.Meta) fmt.Printf("-------------------------------------------\n") - ch <- SignTxResponse{request.transaction, true, ui.readPassword()} + return SignTxResponse{request.Transaction, request.From, true, ui.readPassword()}, nil } // ApproveSignData prompt the user for confirmation to request to sign data -func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest, metadata Metadata, ch chan SignDataResponse) { +func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) { ui.mu.Lock() defer ui.mu.Unlock() 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("Account: %x\n", request.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 <- SignDataResponse{true, ui.readPassword()} + showMetadata(request.Meta) + return SignDataResponse{true, ui.readPassword()}, nil } -// ApproveExport prompt the user for confirmation to export encrypted account json -func (ui *CommandlineUI) ApproveExport(request *ExportRequest, metadata Metadata, ch chan ExportResponse) { +// ApproveExport prompt the user for confirmation to export encrypted Account json +func (ui *CommandlineUI) ApproveExport(request *ExportRequest) (ExportResponse, error) { ui.mu.Lock() defer ui.mu.Unlock() - fmt.Printf("-------- Export account request--------------\n") + 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("Account: %x\n", request.Address) + //fmt.Printf("keyfile: \n%v\n", request.file) fmt.Printf("-------------------------------------------\n") - showMetadata(metadata) - ch <- ExportResponse{ui.confirm()} + showMetadata(request.Meta) + return ExportResponse{ui.confirm()}, nil } -// ApproveImport prompt the user for confirmation to import account json -func (ui *CommandlineUI) ApproveImport(request *ImportRequest, metadata Metadata, ch chan ImportResponse) { +// ApproveImport prompt the user for confirmation to import Account json +func (ui *CommandlineUI) ApproveImport(request *ImportRequest) (ImportResponse, error) { ui.mu.Lock() defer ui.mu.Unlock() - fmt.Printf("-------- Export account request--------------\n") + fmt.Printf("-------- Export Account request--------------\n") fmt.Printf("A request has been made to import an encrypted keyfile\n") fmt.Printf("-------------------------------------------\n") - showMetadata(metadata) - if ui.confirm() { - ch <- ImportResponse{true, ui.readPasswordText("Old password"), ui.readPasswordText("New password")} - } else { - ch <- ImportResponse{false, "", ""} + showMetadata(request.Meta) + if !ui.confirm() { + return ImportResponse{false, "", ""}, nil } + return ImportResponse{true, ui.readPasswordText("Old password"), ui.readPasswordText("New password")}, nil } // 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) { +func (ui *CommandlineUI) ApproveListing(request *ListRequest) (ListResponse, error) { ui.mu.Lock() defer ui.mu.Unlock() - fmt.Printf("-------- List account request--------------\n") + 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 { + for _, account := range request.Accounts { fmt.Printf("\t[x] %v\n", account.Address.Hex()) } fmt.Printf("-------------------------------------------\n") - showMetadata(metadata) - if ui.confirm() { - ch <- ListResponse{request.accounts} - } else { - ch <- ListResponse{nil} + showMetadata(request.Meta) + if !ui.confirm() { + return ListResponse{nil}, nil } + return ListResponse{request.Accounts}, nil } -// 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) { +// ApproveNewAccount prompt the user for confirmation to create new Account, and reveal to caller +func (ui *CommandlineUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) { ui.mu.Lock() defer ui.mu.Unlock() - fmt.Printf("-------- New account request--------------\n") + 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("Approving this operation means that a new Account is created,\n") fmt.Printf("and the address show to the caller\n") - showMetadata(metadata) - ch <- NewAccountResponse{ui.confirm(), ui.readPassword()} + showMetadata(request.Meta) + return NewAccountResponse{ui.confirm(), ui.readPassword()}, nil } // ShowError displays error message to user diff --git a/cmd/signer/main.go b/cmd/signer/main.go index ced6892ef0..de9a864837 100644 --- a/cmd/signer/main.go +++ b/cmd/signer/main.go @@ -30,13 +30,14 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" "gopkg.in/urfave/cli.v1" + "io" ) func main() { app := cli.NewApp() app.Name = "signer" - app.Usage = "Manage ethereum account operations" + app.Usage = "Manage ethereum Account operations" app.Flags = []cli.Flag{ cli.Int64Flag{ Name: "chainid", @@ -77,12 +78,33 @@ func main() { Usage: "File containing requests to handle", Value: "", }, + cli.BoolFlag{ + Name: "stdio-ui", + Usage: "Use STDIN/STDOUT as a channel for an external UI. " + + "This means that an STDIN/STDOUT is used for RPC-communication with a e.g. a graphical user " + + "interface, and can be used when the signer is started by an external process.", + }, } 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)))) + var ( + ui SignerUI + logOutput io.Writer + ) + + if c.Bool("stdio-ui") { + logOutput = os.Stderr + ui = NewStdIOUI() + } else { + ui = NewCommandlineUI() + logOutput = os.Stdout + } + // Set up the logger to print everything + log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int("loglevel")), log.StreamHandler(logOutput, log.TerminalFormat(true)))) + if c.Bool("stdio-ui") { + log.Info("Using stdin/stdout as UI-channel") + } db, err := NewAbiDBFromFile(c.String("4bytedb")) if err != nil { @@ -97,17 +119,18 @@ func main() { c.Int64(utils.NetworkIdFlag.Name), c.String("keystore"), c.Bool(utils.NoUSBFlag.Name), - NewCommandlineUI(), db, + ui, db, c.Bool(utils.LightKDFFlag.Name)) listener net.Listener ) + // Audit logging if logfile := c.String("auditlog"); logfile != "" { f, err := os.OpenFile(logfile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) if err != nil { utils.Fatalf("Could not open %v for audit logging", logfile) } server.SetAuditLogger(NewAuditLogger(f)) - log.Info("Writing audit logs to %v", logfile) + log.Info("Audit logs configured", "file", logfile) } // register signer API with server if err = server.RegisterName("account", api); err != nil { @@ -125,24 +148,23 @@ func main() { 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)) + log.Info("HTTP endpoint opened", "url", fmt.Sprintf("http://%s", endpoint)) cors := []string{"*"} rpc.NewHTTPServer(cors, server).Serve(listener) - return nil } app.Run(os.Args) } -// Create account +// 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/ -// Make transaction +// Make Transaction // safeSend(0x12) // 4401a6e40000000000000000000000000000000000000000000000000000000000000012 diff --git a/cmd/signer/pythonsigner.py b/cmd/signer/pythonsigner.py new file mode 100644 index 0000000000..fc469a96e1 --- /dev/null +++ b/cmd/signer/pythonsigner.py @@ -0,0 +1,100 @@ +import os,sys, subprocess +from tinyrpc.transports import ServerTransport +from tinyrpc.protocols.jsonrpc import JSONRPCProtocol +from tinyrpc.dispatch import RPCDispatcher +from tinyrpc.server import RPCServer + +""" This is a POC example of how to write a custom UI for the signer. The UI starts the +signer process with the '--stdio-ui' option, and communicates with the signer binary +using standard input / output. + +The standard input/output is a relatively secure way to communicate, as it does not require opening any ports +or IPC files. Needless to say, it does not protect against memory inspection mechanisms where an attacker +can access process memory.""" + +try: + import urllib.parse as urlparse +except ImportError: + import urllib as urlparse + +class StdIOTransport(ServerTransport): + """ Uses std input/output for RPC """ + def receive_message(self): + return None, urlparse.unquote(sys.stdin.readline()) + + def send_reply(self, context, reply): + print(reply) + +class PipeTransport(ServerTransport): + """ Uses std a pipe for RPC """ + + def __init__(self,input, output): + self.input = input + self.output = output + + def receive_message(self): + data = self.input.readline() + print("IN ->\n{}".format( data)) + return None, urlparse.unquote(data) + + def send_reply(self, context, reply): + print("OUT <-\n{}".format( reply)) + self.output.write(reply) + self.output.write("\n") + +dispatcher = RPCDispatcher() + +@dispatcher.public +def ApproveTx(Transaction = None, From = None, Callinfo = None, Meta = None): + return { + "Approved" : True, + "Transaction" : Transaction, + "From" : From, + "Password" : None, + } + +@dispatcher.public +def ApproveSignData(): + return {"Approved": False, + "Password" : None} + +@dispatcher.public +def ApproveExport(): + return {"Approved" : False} + +@dispatcher.public +def ApproveImport(): + return {"Approved" : False, "OldPassword": "", "NewPassword": ""} + +@dispatcher.public +def ApproveListing(): + return [] + +@dispatcher.public +def ApproveNewAccount(): + return {"Approved": False, "Password": ""} + +@dispatcher.public +def ShowError(text = ""): + sys.err.println("Error: %s", text) + return + +@dispatcher.public +def ShowInfo(text = ""): + sys.err.println("Info: %s", text) + return + + +def main(): + # line buffered + p = subprocess.Popen(["./signer", "--stdio-ui"], bufsize=1, universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + transport = PipeTransport(p.stdout, p.stdin) + rpc_server = RPCServer( + transport, + JSONRPCProtocol(), + dispatcher + ) + rpc_server.serve_forever() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/cmd/signer/stdioui.go b/cmd/signer/stdioui.go new file mode 100644 index 0000000000..d5472956a1 --- /dev/null +++ b/cmd/signer/stdioui.go @@ -0,0 +1,128 @@ +// 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" + "github.com/ethereum/go-ethereum/log" + "github.com/powerman/rpc-codec/jsonrpc2" + "io" + "os" + "sync" +) + +type StdIOUI struct { + client *jsonrpc2.Client + // codec rpc.ClientCodec + mu sync.Mutex +} + +func NewStdIOUI() *StdIOUI { + in, out := bufio.NewReader(os.Stdin), os.Stdout + + //codec := rpc2.NewJSONCodec() + return &StdIOUI{client: jsonrpc2.NewClient(&rwc{in, out})} + //return &StdIOUI{} +} + +func (ui StdIOUI) dispatch(serviceMethod string, args interface{}, reply interface{}) error { + + // ui.mu.Lock() + // defer ui.mu.Unlock() + //This is not synchronized, which should not be necssary. Ideally, the UI should be able + // to get requests and send responses out-of-order -- thus the rpc has an ID. + + // in, out := bufio.NewReader(os.Stdin), os.Stdout + // codec := jsonrpc.NewClientCodec(&rwc{in, out}) + + // c := rpc.NewClientWithCodec(codec) + // return c.Call(serviceMethod, args, &reply) + + err := ui.client.Call(serviceMethod, args, &reply) + if err != nil { + log.Info("Error", "exc", err.Error()) + } + return err +} + +func (ui StdIOUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) { + result := SignTxResponse{} + if err := ui.dispatch("ApproveTx", request, &result); err != nil { + return result, err + } + return result, nil +} + +func (ui StdIOUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) { + var result SignDataResponse + if err := ui.dispatch("ApproveSignData", request, &result); err != nil { + return result, err + } + return result, nil +} + +func (ui StdIOUI) ApproveExport(request *ExportRequest) (ExportResponse, error) { + var result ExportResponse + if err := ui.dispatch("ApproveExport", request, &result); err != nil { + return result, err + } + return result, nil +} + +func (ui StdIOUI) ApproveImport(request *ImportRequest) (ImportResponse, error) { + var result ImportResponse + if err := ui.dispatch("ApproveImport", request, &result); err != nil { + return result, err + } + return result, nil +} + +func (ui StdIOUI) ApproveListing(request *ListRequest) (ListResponse, error) { + var result ListResponse + if err := ui.dispatch("ApproveListing", request, &result); err != nil { + return result, err + } + return result, nil +} + +func (ui StdIOUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) { + var result NewAccountResponse + if err := ui.dispatch("ApproveNewAccount", request, &result); err != nil { + return result, err + } + return result, nil +} + +func (ui StdIOUI) ShowError(message string) { +} + +func (ui StdIOUI) ShowInfo(message string) { +} + +type rwc struct { + io.Reader + io.Writer +} + +func (r *rwc) Close() error { + if err := os.Stdin.Close(); err != nil { + return err + } + return os.Stdout.Close() + //return nil +} diff --git a/cmd/signer/types.go b/cmd/signer/types.go index 8ac693ab19..dce8c7179b 100644 --- a/cmd/signer/types.go +++ b/cmd/signer/types.go @@ -48,7 +48,7 @@ func (a Account) String() string { return err.Error() } -// TransactionArg represents a transaction for the signer. +// TransactionArg represents a Transaction for the signer. type TransactionArg struct { To *common.Address `json:"to"` Gas *hexutil.Big `json:"gas"`