cmd/signer: implement mixcase addresses in API, fix json id bug

This commit is contained in:
Martin Holst Swende 2017-12-17 23:34:35 +01:00
parent 2ccae4ffec
commit c55fd329ae
No known key found for this signature in database
GPG key ID: 683B438C05A5DDF0
14 changed files with 277 additions and 194 deletions

View file

@ -386,6 +386,8 @@ A UI should conform to the following rules.
* A UI MUST NOT load any external resources that were not embedded/part of the UI package. * A UI MUST NOT load any external resources that were not embedded/part of the UI package.
* For example, not load icons, stylesheets from the internet * For example, not load icons, stylesheets from the internet
* Not load files from the filesystem, unless they reside in the same local directory (e.g. config files) * Not load files from the filesystem, unless they reside in the same local directory (e.g. config files)
* A Graphical UI MUST show the blocky-identicon for ethereum addresses.
* A UI MUST warn display approproate warning if the destination-account is formatted with invalid checksum.
* A UI MUST NOT open any ports or services * A UI MUST NOT open any ports or services
* The signer opens the public port * The signer opens the public port
* A UI SHOULD verify the permissions on the signer binary, and refuse to execute or warn if permissions allow non-user write. * A UI SHOULD verify the permissions on the signer binary, and refuse to execute or warn if permissions allow non-user write.
@ -400,15 +402,8 @@ along with the UI.
Some snags and todos Some snags and todos
* Currently, the API does not make it possible for the signer to forward data about the
checksum, since the addresses are common.Address, and not String. This should be changed upstream,
so that they are some more complex form with both common.Address and the original string (?)
* The audit-log perhaps leave some things to be desired. I have not found a perfect way to save an audit log of events. * The audit-log perhaps leave some things to be desired. I have not found a perfect way to save an audit log of events.
* Some more fields should be added to calldata, e.g http-header `Origin`.
* The signer should take a startup param "--no-change", for UI:s that do not contain the capability * The signer should take a startup param "--no-change", for UI:s that do not contain the capability
to perform changes to things, only approve/deny. Such a UI should be able to start the signer in to perform changes to things, only approve/deny. Such a UI should be able to start the signer in
a more secure mode by telling it that it only wants approve/deny capabilities. a more secure mode by telling it that it only wants approve/deny capabilities.
@ -434,6 +429,11 @@ put together is a bit of a hack into the http server. This could probably be gre
* Geth relay * Geth relay
- Geth should be started in `geth --external_signer localhost:8550`. - Geth should be started in `geth --external_signer localhost:8550`.
* Geth checksum
- Currently, the Geth API:s use `common.Address` in the arguments to transaction submission (e.g `to` field). This
type is 20 `bytes`, and is incapable of carrying checksum information. The signer uses `common.MixedcaseAddress`, which
retains the original input.
- The Geth api should switch to use the same type, and relay `to`-account verbatim to the external api.
* Wallets / accounts. Add API methods for wallets. * Wallets / accounts. Add API methods for wallets.

View file

@ -20,11 +20,12 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"io/ioutil" "io/ioutil"
"strings" "strings"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"regexp" "regexp"
) )

View file

@ -18,10 +18,11 @@ package main
import ( import (
"fmt" "fmt"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"strings" "strings"
"testing" "testing"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
) )
func TestCalldataDecoding(t *testing.T) { func TestCalldataDecoding(t *testing.T) {

View file

@ -25,6 +25,7 @@ import (
"math/big" "math/big"
"bytes" "bytes"
"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"
"github.com/ethereum/go-ethereum/accounts/usbwallet" "github.com/ethereum/go-ethereum/accounts/usbwallet"
@ -55,7 +56,7 @@ type (
// SignTxRequest contains info about a Transaction to sign // SignTxRequest contains info about a Transaction to sign
SignTxRequest struct { SignTxRequest struct {
Transaction TransactionArg `json:"transaction"` Transaction TransactionArg `json:"transaction"`
From common.Address `json:"fromaccount"` From common.MixedcaseAddress `json:"fromaccount"`
Callinfo string `json:"call_info"` Callinfo string `json:"call_info"`
Meta Metadata `json:"meta"` Meta Metadata `json:"meta"`
} }
@ -63,7 +64,7 @@ type (
SignTxResponse struct { SignTxResponse struct {
//The UI may make changes to the TX //The UI may make changes to the TX
Transaction TransactionArg `json:"transaction"` Transaction TransactionArg `json:"transaction"`
From common.Address `json:"fromaccount"` From common.MixedcaseAddress `json:"fromaccount"`
Approved bool `json:"approved"` Approved bool `json:"approved"`
Password string `json:"password"` Password string `json:"password"`
} }
@ -86,7 +87,7 @@ type (
NewPassword string `json:"new_password"` NewPassword string `json:"new_password"`
} }
SignDataRequest struct { SignDataRequest struct {
Address common.Address `json:"address"` Address common.MixedcaseAddress `json:"address"`
Rawdata hexutil.Bytes `json:"raw_data"` Rawdata hexutil.Bytes `json:"raw_data"`
Message string `json:"message"` Message string `json:"message"`
Hash hexutil.Bytes `json:"hash"` Hash hexutil.Bytes `json:"hash"`
@ -111,7 +112,7 @@ type (
Accounts []Account `json:"accounts"` Accounts []Account `json:"accounts"`
} }
Message struct { Message struct {
Message string `json:"message"` Text string `json:"text"`
} }
) )
@ -246,7 +247,7 @@ func toTransaction(args *TransactionArg) *types.Transaction {
if args.To == nil { if args.To == nil {
return types.NewContractCreation(uint64(*args.Nonce), (*big.Int)(args.Value), (*big.Int)(args.Gas), (*big.Int)(args.GasPrice), args.Data) return types.NewContractCreation(uint64(*args.Nonce), (*big.Int)(args.Value), (*big.Int)(args.Gas), (*big.Int)(args.GasPrice), args.Data)
} else { } else {
return types.NewTransaction(uint64(*args.Nonce), *args.To, (*big.Int)(args.Value), (*big.Int)(args.Gas), (*big.Int)(args.GasPrice), args.Data) return types.NewTransaction(uint64(*args.Nonce), args.To.Address(), (*big.Int)(args.Value), (*big.Int)(args.Gas), (*big.Int)(args.GasPrice), args.Data)
} }
} }
@ -260,11 +261,9 @@ func logDiff(original *SignTxRequest, new *SignTxResponse) bool {
log.Info("Sender-account changed by UI", "was", f0, "is", f1) log.Info("Sender-account changed by UI", "was", f0, "is", f1)
} }
if t0, t1 := original.Transaction.To, new.Transaction.To; t0 != t1 { 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) log.Info("Recipient-account changed by UI", "was", t0, "is", t1)
modified = true modified = true
} }
}
if g0, g1 := (*big.Int)(original.Transaction.Gas), (*big.Int)(new.Transaction.Gas); g0 != g1 { if g0, g1 := (*big.Int)(original.Transaction.Gas), (*big.Int)(new.Transaction.Gas); g0 != g1 {
if g0 == nil || g1 == nil || g0.Cmp(g1) != 0 { if g0 == nil || g1 == nil || g0.Cmp(g1) != 0 {
modified = true modified = true
@ -299,7 +298,7 @@ func logDiff(original *SignTxRequest, new *SignTxResponse) bool {
// 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
// that can be posted to `eth_sendRawTransaction`. // that can be posted to `eth_sendRawTransaction`.
func (api *SignerAPI) SignTransaction(ctx context.Context, from common.Address, args TransactionArg, methodSelector *string) (hexutil.Bytes, error) { func (api *SignerAPI) SignTransaction(ctx context.Context, from common.MixedcaseAddress, args TransactionArg, methodSelector *string) (hexutil.Bytes, error) {
var ( var (
err error err error
@ -350,7 +349,7 @@ func (api *SignerAPI) SignTransaction(ctx context.Context, from common.Address,
acc accounts.Account acc accounts.Account
wallet accounts.Wallet wallet accounts.Wallet
) )
acc = accounts.Account{Address: result.From} acc = accounts.Account{Address: result.From.Address()}
wallet, err = api.am.Find(acc) wallet, err = api.am.Find(acc)
if err != nil { if err != nil {
return nil, err return nil, err
@ -376,7 +375,7 @@ func (api *SignerAPI) SignTransaction(ctx context.Context, from common.Address,
// The key used to calculate the signature is decrypted with the given password. // The key used to calculate the signature is decrypted with the given password.
// //
// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign // 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) { func (api *SignerAPI) Sign(ctx context.Context, addr common.MixedcaseAddress, data hexutil.Bytes) (hexutil.Bytes, error) {
sighash, msg := signHash(data) sighash, msg := signHash(data)
@ -393,7 +392,7 @@ func (api *SignerAPI) Sign(ctx context.Context, addr common.Address, data hexuti
} }
// Look up the wallet containing the requested signer // Look up the wallet containing the requested signer
account := accounts.Account{Address: addr} account := accounts.Account{Address: addr.Address()}
wallet, err := api.am.Find(account) wallet, err := api.am.Find(account)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -4,18 +4,19 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"github.com/ethereum/go-ethereum/accounts/keystore"
"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"
"github.com/ethereum/go-ethereum/rlp"
"io/ioutil" "io/ioutil"
"math/big" "math/big"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"time" "time"
"github.com/ethereum/go-ethereum/accounts/keystore"
"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"
"github.com/ethereum/go-ethereum/rlp"
) )
//Used for testing //Used for testing
@ -198,7 +199,7 @@ func TestSignData(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
a := list[0].Address a := common.NewMixedcaseAddress(list[0].Address)
control <- "Y" control <- "Y"
control <- "wrongpassword" control <- "wrongpassword"
@ -231,7 +232,7 @@ func TestSignData(t *testing.T) {
} }
} }
func mkTestTx() TransactionArg { func mkTestTx() TransactionArg {
to := common.HexToAddress("0x1337") to := common.NewMixedcaseAddress(common.HexToAddress("0x1337"))
gas := (*hexutil.Big)(big.NewInt(21000)) gas := (*hexutil.Big)(big.NewInt(21000))
gasPrice := (*hexutil.Big)(big.NewInt(2000000000)) gasPrice := (*hexutil.Big)(big.NewInt(2000000000))
value := (*hexutil.Big)(big.NewInt(1e18)) value := (*hexutil.Big)(big.NewInt(1e18))
@ -262,7 +263,7 @@ func TestSignTx(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
a := list[0].Address a := common.NewMixedcaseAddress(list[0].Address)
methodSig := "test(uint)" methodSig := "test(uint)"
tx := mkTestTx() tx := mkTestTx()

View file

@ -3,9 +3,10 @@ package main
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"github.com/ethereum/go-ethereum/rpc"
"io" "io"
"time" "time"
"github.com/ethereum/go-ethereum/rpc"
) )
type AuditLogger struct { type AuditLogger struct {

View file

@ -21,10 +21,11 @@ import (
"os" "os"
"strings" "strings"
"sync"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/crypto/ssh/terminal"
"sync"
) )
type CommandlineUI struct { type CommandlineUI struct {
@ -98,13 +99,16 @@ func (ui *CommandlineUI) ApproveTx(request *SignTxRequest) (SignTxResponse, erro
ui.mu.Lock() ui.mu.Lock()
defer ui.mu.Unlock() defer ui.mu.Unlock()
weival := request.Transaction.Value.ToInt() weival := request.Transaction.Value.ToInt()
toval := ""
if request.Transaction.To != nil {
toval = request.Transaction.To.Hex()
}
fmt.Printf("--------- Transaction request-------------\n") fmt.Printf("--------- Transaction request-------------\n")
fmt.Printf("to: %v\n", toval) if to := request.Transaction.To; to != nil {
fmt.Printf("from: %v\n", request.From.Hex()) fmt.Printf("to: %v\n", to.Original())
if !to.ValidChecksum() {
fmt.Printf("\nWARNING: Invalid checksum on to-address!\n\n")
}
} else {
fmt.Printf("to: <contact creation>\n")
}
fmt.Printf("from: %v\n", request.From.String())
fmt.Printf("value: %v wei\n", weival) fmt.Printf("value: %v wei\n", weival)
if len(request.Transaction.Data) > 0 { if len(request.Transaction.Data) > 0 {
fmt.Printf("data: %v\n", common.Bytes2Hex(request.Transaction.Data)) fmt.Printf("data: %v\n", common.Bytes2Hex(request.Transaction.Data))

View file

@ -26,6 +26,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
@ -33,7 +34,6 @@ import (
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
"gopkg.in/urfave/cli.v1" "gopkg.in/urfave/cli.v1"
"io"
) )
func main() { func main() {
@ -97,18 +97,19 @@ func main() {
var ( var (
ui SignerUI ui SignerUI
logOutput io.Writer
) )
// Set up the logger to print everything
logOutput := os.Stdout
if c.Bool("stdio-ui") { if c.Bool("stdio-ui") {
logOutput = os.Stderr logOutput = os.Stderr
}
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int("loglevel")), log.StreamHandler(logOutput, log.TerminalFormat(true))))
if c.Bool("stdio-ui") {
ui = NewStdIOUI() ui = NewStdIOUI()
} else { } else {
ui = NewCommandlineUI() 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") { if c.Bool("stdio-ui") {
log.Info("Using stdin/stdout as UI-channel") log.Info("Using stdin/stdout as UI-channel")
} }
@ -190,9 +191,9 @@ func testExternalUI(api *SignerAPI) {
} }
var err error var err error
_, err = api.SignTransaction(ctx, common.Address{}, TransactionArg{}, nil) _, err = api.SignTransaction(ctx, common.MixedcaseAddress{}, TransactionArg{}, nil)
checkErr("SignTransaction", err) checkErr("SignTransaction", err)
_, err = api.Sign(ctx, common.Address{}, common.Hex2Bytes("01020304")) _, err = api.Sign(ctx, common.MixedcaseAddress{}, common.Hex2Bytes("01020304"))
checkErr("Sign", err) checkErr("Sign", err)
_, err = api.List(ctx) _, err = api.List(ctx)
checkErr("List", err) checkErr("List", err)

View file

@ -1,7 +1,7 @@
import os,sys, subprocess import os,sys, subprocess
from tinyrpc.transports import ServerTransport from tinyrpc.transports import ServerTransport
from tinyrpc.protocols.jsonrpc import JSONRPCProtocol from tinyrpc.protocols.jsonrpc import JSONRPCProtocol
from tinyrpc.dispatch import RPCDispatcher from tinyrpc.dispatch import public,RPCDispatcher
from tinyrpc.server import RPCServer from tinyrpc.server import RPCServer
""" This is a POC example of how to write a custom UI for the signer. The UI starts the """ This is a POC example of how to write a custom UI for the signer. The UI starts the
@ -34,22 +34,25 @@ class PipeTransport(ServerTransport):
def receive_message(self): def receive_message(self):
data = self.input.readline() data = self.input.readline()
#print(">> {}".format( data)) print(">> {}".format( data))
return None, urlparse.unquote(data) return None, urlparse.unquote(data)
def send_reply(self, context, reply): def send_reply(self, context, reply):
#print("<< {}".format( reply)) print("<< {}".format( reply))
self.output.write(reply) self.output.write(reply)
self.output.write("\n") self.output.write("\n")
dispatcher = RPCDispatcher() class StdIOHandler():
@dispatcher.public def __init__(self):
def ApproveTx(transaction = None, fromaccount = None, call_info = None, meta = None): pass
@public
def ApproveTx(self,transaction = None, fromaccount = None, call_info = None, meta = None):
""" """
Example request: Example request:
{"jsonrpc":"2.0","method":"ApproveTx","params":{"transaction":{"to":null,"gas":null,"gasPrice":null,"value":null,"data":"0x","nonce":null},"fromaccount":"0x0000000000000000000000000000000000000000","call_info":null,"meta":{"remote":"signer binary","local":"main","scheme":"in-proc"}},"id":2} {"jsonrpc":"2.0","method":"ApproveTx","params":{"transaction":{"to":null,"gas":null,"gasPrice":null,"value":null,"data":"0x","nonce":null},"from":"0x0000000000000000000000000000000000000000","call_info":null,"meta":{"remote":"signer binary","local":"main","scheme":"in-proc"}},"id":2}
:param transaction: transaction info :param transaction: transaction info
:param call_info: info abou the call, e.g. if ABI info could not be :param call_info: info abou the call, e.g. if ABI info could not be
@ -59,12 +62,12 @@ def ApproveTx(transaction = None, fromaccount = None, call_info = None, meta = N
return { return {
"approved" : False, "approved" : False,
"transaction" : None, "transaction" : None,
# "fromaccount" : fromaccount, #"fromaccount" : fromaccount,
"password" : None, "password" : None,
} }
@dispatcher.public @public
def ApproveSignData(address=None, raw_data = None, message = None, hash = None, meta = None): def ApproveSignData(self,address=None, raw_data = None, message = None, hash = None, meta = None):
""" Example request """ Example request
{"jsonrpc":"2.0","method":"ApproveSignData","params":{"address":"0x0000000000000000000000000000000000000000","raw_data":"0x01020304","message":"\u0019Ethereum Signed Message:\n4\u0001\u0002\u0003\u0004","hash":"0x7e3a4e7a9d1744bc5c675c25e1234ca8ed9162bd17f78b9085e48047c15ac310","meta":{"remote":"signer binary","local":"main","scheme":"in-proc"}},"id":3} {"jsonrpc":"2.0","method":"ApproveSignData","params":{"address":"0x0000000000000000000000000000000000000000","raw_data":"0x01020304","message":"\u0019Ethereum Signed Message:\n4\u0001\u0002\u0003\u0004","hash":"0x7e3a4e7a9d1744bc5c675c25e1234ca8ed9162bd17f78b9085e48047c15ac310","meta":{"remote":"signer binary","local":"main","scheme":"in-proc"}},"id":3}
@ -74,8 +77,8 @@ def ApproveSignData(address=None, raw_data = None, message = None, hash = None,
return {"approved": False, return {"approved": False,
"password" : None} "password" : None}
@dispatcher.public @public
def ApproveExport(address = None, meta = None): def ApproveExport(self,address = None, meta = None):
""" Example request """ Example request
{"jsonrpc":"2.0","method":"ApproveExport","params":{"address":"0x0000000000000000000000000000000000000000","meta":{"remote":"signer binary","local":"main","scheme":"in-proc"}},"id":5} {"jsonrpc":"2.0","method":"ApproveExport","params":{"address":"0x0000000000000000000000000000000000000000","meta":{"remote":"signer binary","local":"main","scheme":"in-proc"}},"id":5}
@ -83,8 +86,8 @@ def ApproveExport(address = None, meta = None):
""" """
return {"approved" : False} return {"approved" : False}
@dispatcher.public @public
def ApproveImport(meta = None): def ApproveImport(self,meta = None):
""" Example request """ Example request
{"jsonrpc":"2.0","method":"ApproveImport","params":{"Meta":{}},"id":4} {"jsonrpc":"2.0","method":"ApproveImport","params":{"Meta":{}},"id":4}
@ -92,16 +95,16 @@ def ApproveImport(meta = None):
""" """
return {"approved" : False, "old_password": "", "new_password": ""} return {"approved" : False, "old_password": "", "new_password": ""}
@dispatcher.public @public
def ApproveListing(accounts=None, meta = None): def ApproveListing(self,accounts=None, meta = None):
""" Example request """ Example request
{"jsonrpc":"2.0","method":"ApproveListing","params":{"accounts":[{"type":"Account","url":"keystore:///home/user/ethereum/keystore/file","address":"0x010101010101010010101010101abcdef0001337"}],"Meta":{}},"id":2} {"jsonrpc":"2.0","method":"ApproveListing","params":{"accounts":[{"type":"Account","url":"keystore:///home/user/ethereum/keystore/file","address":"0x010101010101010010101010101abcdef0001337"}],"Meta":{}},"id":2}
""" """
return {'accounts': []} return {'accounts': []}
@dispatcher.public @public
def ApproveNewAccount(meta = None): def ApproveNewAccount(self,meta = None):
""" """
Example request Example request
@ -111,43 +114,46 @@ def ApproveNewAccount(meta = None):
""" """
return {"approved": False, "password": ""} return {"approved": False, "password": ""}
@dispatcher.public @public
def ShowError(message = ""): def ShowError(self,message = {}):
""" """
Example request: Example request:
{"jsonrpc":"2.0","method":"ShowInfo","params":{"message":"Testing 'ShowError'"},"id":1} {"jsonrpc":"2.0","method":"ShowInfo","params":{"message":"Testing 'ShowError'"},"id":1}
:param text: to show :param message: to show
:return: nothing :return: nothing
""" """
sys.stderr.write("Error: {}\n".format( message)) if 'text' in message.keys():
sys.stderr.write("Error: {}\n".format( message['text']))
return return
@dispatcher.public @public
def ShowInfo(message = ""): def ShowInfo(self,message = {}):
""" """
Example request Example request
{"jsonrpc":"2.0","method":"ShowInfo","params":{"message":"Testing 'ShowInfo'"},"id":0} {"jsonrpc":"2.0","method":"ShowInfo","params":{"message":"Testing 'ShowInfo'"},"id":0}
:param text: to display :param message: to display
:return:nothing :return:nothing
""" """
sys.stdout.write("Info: {}\n".format( message)) if 'text' in message.keys():
sys.stdout.write("Error: {}\n".format( message['text']))
return return
def main(args): def main(args):
cmd = ["./signer", "--stdio-ui"] cmd = ["./signer", "--stdio-ui"]
if len(args) > 0 and args[0] == "test": if len(args) > 0 and args[0] == "test":
cmd.extend(["--stdio-ui-test"]) cmd.extend(["--stdio-ui-test"])
print("cmd: {}".format(" ".join(cmd))) print("cmd: {}".format(" ".join(cmd)))
dispatcher = RPCDispatcher()
dispatcher.register_instance(StdIOHandler(), '')
# line buffered # line buffered
p = subprocess.Popen(cmd, bufsize=1, universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE) p = subprocess.Popen(cmd, bufsize=1, universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
transport = PipeTransport(p.stdout, p.stdin)
rpc_server = RPCServer( rpc_server = RPCServer(
transport, PipeTransport(p.stdout, p.stdin),
JSONRPCProtocol(), JSONRPCProtocol(),
dispatcher dispatcher
) )

View file

@ -20,11 +20,10 @@ package main
import ( import (
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
"io"
"os"
"sync" "sync"
"github.com/ethereum/go-ethereum/rpc"
"context" "context"
) )
@ -36,47 +35,33 @@ type StdIOUI struct {
} }
func NewStdIOUI() *StdIOUI { func NewStdIOUI() *StdIOUI {
log.Info("NewStdIOUI")
// in, out := bufio.NewReader(os.Stdin), os.Stdout // in, out := bufio.NewReader(os.Stdin), os.Stdout
client, err := rpc.DialContext(context.Background(), "stdio://") client, err := rpc.DialContext(context.Background(), "stdio://")
if err != nil { if err != nil {
log.Crit("Could not create stdio client", "err", err) log.Crit("Could not create stdio client", "err", err)
} }
return &StdIOUI{client: *client} return &StdIOUI{client: *client}
//return &StdIOUI{client: jsonrpc2.NewClient(&rwc{in, out})}
} }
func (ui StdIOUI) dispatch(serviceMethod string, args interface{}, reply interface{}) error { // dispatch sends a request over the stdio
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)
log.Info("Writing to client")
err := ui.client.Call(&reply, serviceMethod, args) err := ui.client.Call(&reply, serviceMethod, args)
log.Info("Writing to client done")
// err := ui.client.Call(serviceMethod, args, &reply)
if err != nil { if err != nil {
log.Info("Error", "exc", err.Error()) log.Info("Error", "exc", err.Error())
} }
return err return err
} }
func (ui StdIOUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) { func (ui *StdIOUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) {
result := SignTxResponse{} var result SignTxResponse
if err := ui.dispatch("ApproveTx", request, &result); err != nil { if err := ui.dispatch("ApproveTx", request, &result); err != nil {
return result, err return result, err
} }
return result, nil return result, nil
} }
func (ui StdIOUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) { func (ui *StdIOUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) {
var result SignDataResponse var result SignDataResponse
if err := ui.dispatch("ApproveSignData", request, &result); err != nil { if err := ui.dispatch("ApproveSignData", request, &result); err != nil {
return result, err return result, err
@ -84,7 +69,7 @@ func (ui StdIOUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, e
return result, nil return result, nil
} }
func (ui StdIOUI) ApproveExport(request *ExportRequest) (ExportResponse, error) { func (ui *StdIOUI) ApproveExport(request *ExportRequest) (ExportResponse, error) {
var result ExportResponse var result ExportResponse
if err := ui.dispatch("ApproveExport", request, &result); err != nil { if err := ui.dispatch("ApproveExport", request, &result); err != nil {
return result, err return result, err
@ -92,7 +77,7 @@ func (ui StdIOUI) ApproveExport(request *ExportRequest) (ExportResponse, error)
return result, nil return result, nil
} }
func (ui StdIOUI) ApproveImport(request *ImportRequest) (ImportResponse, error) { func (ui *StdIOUI) ApproveImport(request *ImportRequest) (ImportResponse, error) {
var result ImportResponse var result ImportResponse
if err := ui.dispatch("ApproveImport", request, &result); err != nil { if err := ui.dispatch("ApproveImport", request, &result); err != nil {
return result, err return result, err
@ -100,7 +85,7 @@ func (ui StdIOUI) ApproveImport(request *ImportRequest) (ImportResponse, error)
return result, nil return result, nil
} }
func (ui StdIOUI) ApproveListing(request *ListRequest) (ListResponse, error) { func (ui *StdIOUI) ApproveListing(request *ListRequest) (ListResponse, error) {
var result ListResponse var result ListResponse
if err := ui.dispatch("ApproveListing", request, &result); err != nil { if err := ui.dispatch("ApproveListing", request, &result); err != nil {
return result, err return result, err
@ -108,7 +93,7 @@ func (ui StdIOUI) ApproveListing(request *ListRequest) (ListResponse, error) {
return result, nil return result, nil
} }
func (ui StdIOUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) { func (ui *StdIOUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) {
var result NewAccountResponse var result NewAccountResponse
if err := ui.dispatch("ApproveNewAccount", request, &result); err != nil { if err := ui.dispatch("ApproveNewAccount", request, &result); err != nil {
return result, err return result, err
@ -116,29 +101,16 @@ func (ui StdIOUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountRespo
return result, nil return result, nil
} }
func (ui StdIOUI) ShowError(message string) { func (ui *StdIOUI) ShowError(message string) {
err := ui.dispatch("ShowError", &Message{message}, nil) err := ui.dispatch("ShowError", &Message{message}, nil)
if err != nil { if err != nil {
log.Info("Error calling 'ShowError'", "exc", err.Error(), "msg", message) log.Info("Error calling 'ShowError'", "exc", err.Error(), "msg", message)
} }
} }
func (ui StdIOUI) ShowInfo(message string) { func (ui *StdIOUI) ShowInfo(message string) {
err := ui.dispatch("ShowInfo", Message{message}, nil) err := ui.dispatch("ShowInfo", Message{message}, nil)
if err != nil { if err != nil {
log.Info("Error calling 'ShowInfo'", "exc", err.Error(), "msg", message) log.Info("Error calling 'ShowInfo'", "exc", err.Error(), "msg", message)
} }
} }
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
}

View file

@ -18,10 +18,11 @@ package main
import ( import (
"encoding/json" "encoding/json"
"strings"
"github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"strings"
) )
type Accounts []Account type Accounts []Account
@ -50,7 +51,7 @@ func (a Account) String() string {
// TransactionArg represents a Transaction for the signer. // TransactionArg represents a Transaction for the signer.
type TransactionArg struct { type TransactionArg struct {
To *common.Address `json:"to"` To *common.MixedcaseAddress `json:"to"`
Gas *hexutil.Big `json:"gas"` Gas *hexutil.Big `json:"gas"`
GasPrice *hexutil.Big `json:"gasPrice"` GasPrice *hexutil.Big `json:"gasPrice"`
Value *hexutil.Big `json:"value"` Value *hexutil.Big `json:"value"`

View file

@ -23,6 +23,7 @@ import (
"math/rand" "math/rand"
"reflect" "reflect"
"encoding/json"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto/sha3" "github.com/ethereum/go-ethereum/crypto/sha3"
) )
@ -240,3 +241,56 @@ func (a *UnprefixedAddress) UnmarshalText(input []byte) error {
func (a UnprefixedAddress) MarshalText() ([]byte, error) { func (a UnprefixedAddress) MarshalText() ([]byte, error) {
return []byte(hex.EncodeToString(a[:])), nil return []byte(hex.EncodeToString(a[:])), nil
} }
// MixedcaseAddress retains the original string, which may or may not be
// correctly checksummed
//
// TODO! Should we really keep both addr and original, or _only_ original, and
// always calculate addr on the fly? That would reduce the possibilities for errors
// if some caller modifies the original at some point. NB: If we do so, we should still
// parse the addr in UnmarshalJSON to ensure that the format is correct, e.g. correct size and
// hex-encoded and such
type MixedcaseAddress struct {
addr Address
original string
}
// NewMixedcaseAddress constructor (mainly for testing)
func NewMixedcaseAddress(addr Address) MixedcaseAddress {
return MixedcaseAddress{addr: addr, original: addr.Hex()}
}
// UnmarshalJSON parses MixedcaseAddress
func (ma *MixedcaseAddress) UnmarshalJSON(input []byte) error {
if err := hexutil.UnmarshalFixedJSON(addressT, input, ma.addr[:]); err != nil {
return err
}
return json.Unmarshal(input, &ma.original)
}
// MarshalJSON marshals the original value
func (ma *MixedcaseAddress) MarshalJSON() ([]byte, error) {
return json.Marshal(ma.original)
}
// Address returns the address
func (ma *MixedcaseAddress) Address() Address {
return ma.addr
}
// String implements fmt.Stringer
func (ma *MixedcaseAddress) String() string {
if ma.ValidChecksum() {
return fmt.Sprintf("%s [chksum ok]", ma.original)
}
return fmt.Sprintf("%s [chksum INVALID]", ma.original)
}
// ValidChecksum returns true if the address has valid checksum
func (ma *MixedcaseAddress) ValidChecksum() bool {
return ma.original == ma.addr.Hex()
}
// Original returns the mixed-case input string
func (ma *MixedcaseAddress) Original() string {
return ma.original
}

View file

@ -18,6 +18,7 @@ package common
import ( import (
"encoding/json" "encoding/json"
"math/big" "math/big"
"strings" "strings"
"testing" "testing"
@ -149,3 +150,46 @@ func BenchmarkAddressHex(b *testing.B) {
testAddr.Hex() testAddr.Hex()
} }
} }
func TestMixedcaseAccount_Address(t *testing.T) {
// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md
// Note: 0X{checksum_addr} is not valid according to spec above
var res []struct {
A MixedcaseAddress
Valid bool
}
if err := json.Unmarshal([]byte(`[
{"A" : "0xae967917c465db8578ca9024c205720b1a3651A9", "Valid": false},
{"A" : "0xAe967917c465db8578ca9024c205720b1a3651A9", "Valid": true},
{"A" : "0XAe967917c465db8578ca9024c205720b1a3651A9", "Valid": false},
{"A" : "0x1111111111111111111112222222222223333323", "Valid": true}
]`), &res); err != nil {
t.Fatal(err)
}
for _, r := range res {
if got := r.A.ValidChecksum(); got != r.Valid {
t.Errorf("Expected checksum %v, got checksum %v, input %v", r.Valid, got, r.A.String())
}
}
//These should throw exceptions:
var r2 []MixedcaseAddress
for _, r := range []string{
`["0x11111111111111111111122222222222233333"]`, // Too short
`["0x111111111111111111111222222222222333332"]`, // Too short
`["0x11111111111111111111122222222222233333234"]`, // Too long
`["0x111111111111111111111222222222222333332344"]`, // Too long
`["1111111111111111111112222222222223333323"]`, // Missing 0x
`["x1111111111111111111112222222222223333323"]`, // Missing 0
`["0xG111111111111111111112222222222223333323"]`, //Non-hex
} {
if err := json.Unmarshal([]byte(r), &r2); err == nil {
t.Errorf("Expected failure, input %v", r)
}
}
}

View file

@ -215,7 +215,6 @@ func (io StdIOConn) SetWriteDeadline(t time.Time) error {
return &net.OpError{Op: "set", Net: "stdio", Source: nil, Addr: nil, Err: errors.New("deadline not supported")} return &net.OpError{Op: "set", Net: "stdio", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
} }
func DialStdIO(ctx context.Context) (*Client, error) { func DialStdIO(ctx context.Context) (*Client, error) {
return newClient(ctx, func(_ context.Context) (net.Conn, error) { return newClient(ctx, func(_ context.Context) (net.Conn, error) {
return StdIOConn{}, nil return StdIOConn{}, nil
}) })
@ -227,7 +226,6 @@ func newClient(initctx context.Context, connectFunc func(context.Context) (net.C
return nil, err return nil, err
} }
_, isHTTP := conn.(*httpConn) _, isHTTP := conn.(*httpConn)
c := &Client{ c := &Client{
writeConn: conn, writeConn: conn,
isHTTP: isHTTP, isHTTP: isHTTP,
@ -567,13 +565,13 @@ func (c *Client) dispatch(conn net.Conn) {
} }
case err := <-c.readErr: case err := <-c.readErr:
log.Debug(fmt.Sprintf("<-readErr: %v", err)) log.Debug("<-readErr", "err", err)
c.closeRequestOps(err) c.closeRequestOps(err)
conn.Close() conn.Close()
reading = false reading = false
case newconn := <-c.reconnected: case newconn := <-c.reconnected:
log.Debug(fmt.Sprintf("<-reconnected: (reading=%t) %v", reading, conn.RemoteAddr())) log.Debug("<-reconnected", "reading", reading, "remote", conn.RemoteAddr())
if reading { if reading {
// Wait for the previous read loop to exit. This is a rare case. // Wait for the previous read loop to exit. This is a rare case.
conn.Close() conn.Close()
@ -630,7 +628,7 @@ func (c *Client) closeRequestOps(err error) {
func (c *Client) handleNotification(msg *jsonrpcMessage) { func (c *Client) handleNotification(msg *jsonrpcMessage) {
if !strings.HasSuffix(msg.Method, notificationMethodSuffix) { if !strings.HasSuffix(msg.Method, notificationMethodSuffix) {
log.Debug(fmt.Sprint("dropping non-subscription message: ", msg)) log.Debug("dropping non-subscription message", "msg", msg)
return return
} }
var subResult struct { var subResult struct {
@ -638,7 +636,7 @@ func (c *Client) handleNotification(msg *jsonrpcMessage) {
Result json.RawMessage `json:"result"` Result json.RawMessage `json:"result"`
} }
if err := json.Unmarshal(msg.Params, &subResult); err != nil { if err := json.Unmarshal(msg.Params, &subResult); err != nil {
log.Debug(fmt.Sprint("dropping invalid subscription message: ", msg)) log.Debug("dropping invalid subscription message", "msg", msg)
return return
} }
if c.subs[subResult.ID] != nil { if c.subs[subResult.ID] != nil {
@ -649,7 +647,7 @@ func (c *Client) handleNotification(msg *jsonrpcMessage) {
func (c *Client) handleResponse(msg *jsonrpcMessage) { func (c *Client) handleResponse(msg *jsonrpcMessage) {
op := c.respWait[string(msg.ID)] op := c.respWait[string(msg.ID)]
if op == nil { if op == nil {
log.Debug(fmt.Sprintf("unsolicited response %v", msg)) log.Debug("unsolicited response", "msg", msg)
return return
} }
delete(c.respWait, string(msg.ID)) delete(c.respWait, string(msg.ID))