cmd/signer, rpc: Implement new signer. Add info about remote user to Context

This commit is contained in:
Martin Holst Swende 2017-11-24 00:56:05 +01:00
parent 6a49fd22c5
commit 53d6353ae1
No known key found for this signature in database
GPG key ID: 683B438C05A5DDF0
5 changed files with 328 additions and 44 deletions

View file

@ -17,15 +17,13 @@
package main package main
import ( import (
"fmt"
"errors"
"context" "context"
"encoding/json"
"math/big" "errors"
"fmt"
"io/ioutil" "io/ioutil"
"math/big"
"os"
"github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/accounts/keystore"
@ -36,12 +34,97 @@ import (
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"encoding/json"
) )
type SignerAPI struct { type SignerAPI struct {
chainID *big.Int chainID *big.Int
am *accounts.Manager 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. // 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. // key that is generated when a new account is created.
// noUSB disables USB support that is required to support hardware devices such as // noUSB disables USB support that is required to support hardware devices such as
// ledger and trezor. // 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 var backends []accounts.Backend
// support password based accounts // 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 // List returns the set of wallet this signer manages. Each wallet can contain
// multiple accounts. // 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 var accounts []Account
for _, wallet := range api.am.Wallets() { for _, wallet := range api.am.Wallets() {
for _, acc := range wallet.Accounts() { for _, acc := range wallet.Accounts() {
@ -87,7 +188,12 @@ func (api *SignerAPI) List(ctx context.Context) []Account {
accounts = append(accounts, acc) 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 // 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 { if len(be) == 0 {
return accounts.Account{}, errors.New("password based accounts not supported") return accounts.Account{}, errors.New("password based accounts not supported")
} }
acc, err := be[0].(*keystore.KeyStore).NewAccount(passphrase) ch := make(chan bool, 1)
return acc, err 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 // 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) 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) ch := make(chan ApprovalStatus, 1)
if err != nil { api.ui.ApproveTx(&SignTxRequest{transaction: tx, from: acc}, metaData(ctx), ch)
return nil, err
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: // 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 { if err != nil {
return nil, err return nil, err
} }
// Assemble sign the data with the wallet ch := make(chan ApprovalStatus, 1)
signature, err := wallet.SignHashWithPassphrase(account, passwd, signHash(data))
if err != nil { msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data)
return nil, err 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 nil, fmt.Errorf("Signing rejected")
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.
@ -207,8 +341,14 @@ func (api *SignerAPI) Export(ctx context.Context, addr common.Address) (json.Raw
if url.Scheme != keystore.KeyStoreScheme { if url.Scheme != keystore.KeyStoreScheme {
return nil, fmt.Errorf("account is not a password protected account") 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 // 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. // 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) { func (api *SignerAPI) Import(ctx context.Context, keyJSON json.RawMessage, passphrase, newPassphrase string) (Account, error) {
be := api.am.Backends(keystore.KeyStoreType) be := api.am.Backends(keystore.KeyStoreType)
if len(be) == 0 { if len(be) == 0 {
return Account{}, errors.New("password based accounts not supported") return Account{}, errors.New("password based accounts not supported")
} }
acc, err := be[0].(*keystore.KeyStore).Import(keyJSON, passphrase, newPassphrase) ch := make(chan ApprovalStatus, 1)
if err != nil {
return Account{}, err
}
return Account{Typ: "account", URL: acc.URL, Address: acc.Address}, nil 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")
}

117
cmd/signer/cliui.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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)
}

View file

@ -19,7 +19,6 @@
package main package main
import ( import (
"bufio"
"flag" "flag"
"io" "io"
"os" "os"
@ -29,6 +28,9 @@ import (
"github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum/go-ethereum/log"
"net"
"fmt"
) )
var ( var (
@ -41,19 +43,29 @@ func main() {
var ( var (
server = rpc.NewServer() server = rpc.NewServer()
api = NewSignerAPI(*chainID, *ksLocation, true) api = NewSignerAPI(*chainID, *ksLocation, true, NewCommandlineUI())
listener net.Listener
err error
) )
// register signer API with server // 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) utils.Fatalf("Could not register signer API: %v", err)
} }
// start server with in-/output connected to stdin/stdout endpoint := "localhost:8550"
in, out := bufio.NewReader(os.Stdin), os.Stdout
codec := rpc.NewJSONCodec(&rwc{in, out}) if listener, err = net.Listen("tcp", endpoint); err != nil {
server.ServeCodec(codec, rpc.OptionMethodInvocation|rpc.OptionSubscriptions) 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 { type rwc struct {
io.Reader io.Reader

View file

@ -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 // 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 // untilEOF and writes the response to w and order the server to process a
// single request. // 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) body := io.LimitReader(r.Body, maxRequestContentLength)
codec := NewJSONCodec(&httpReadWriteNopCloser{body, w}) codec := NewJSONCodec(&httpReadWriteNopCloser{body, w})
defer codec.Close() defer codec.Close()
w.Header().Set("content-type", contentType) 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 // validateRequest returns a non-zero response code and error message if the

View file

@ -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 // 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 // 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. // 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 var pend sync.WaitGroup
defer func() { defer func() {
@ -140,7 +140,8 @@ func (s *Server) serveRequest(codec ServerCodec, singleShot bool, options CodecO
s.codecsMu.Unlock() s.codecsMu.Unlock()
}() }()
ctx, cancel := context.WithCancel(context.Background()) // ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
// if the codec supports notification include a notifier that callbacks can use // 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. // stopped. In either case the codec is closed.
func (s *Server) ServeCodec(codec ServerCodec, options CodecOption) { func (s *Server) ServeCodec(codec ServerCodec, options CodecOption) {
defer codec.Close() 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 // 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 // close the codec unless a non-recoverable error has occurred. Note, this method will return after
// a single request has been processed! // a single request has been processed!
func (s *Server) ServeSingleRequest(codec ServerCodec, options CodecOption) { func (s *Server) ServeSingleRequest(codec ServerCodec, options CodecOption, ctx context.Context) {
s.serveRequest(codec, true, options) s.serveRequest(codec, true, options, ctx)
} }
// Stop will stop reading new requests, wait for stopPendingRequestTimeout to allow pending requests to finish, // Stop will stop reading new requests, wait for stopPendingRequestTimeout to allow pending requests to finish,