aboutsummaryrefslogtreecommitdiffstats
path: root/signer/core
diff options
context:
space:
mode:
authorMartin Holst Swende <martin@swende.se>2018-04-16 20:04:32 +0800
committerPéter Szilágyi <peterke@gmail.com>2018-04-16 20:04:32 +0800
commitec3db0f56c779387132dcf2049ed32bf4ed34a4f (patch)
treed509c580e02053fd133b0402c0838940d4b871d2 /signer/core
parentde2a7bb764c82dbaa80d37939c5862358174bc6e (diff)
downloadgo-tangerine-ec3db0f56c779387132dcf2049ed32bf4ed34a4f.tar.gz
go-tangerine-ec3db0f56c779387132dcf2049ed32bf4ed34a4f.tar.zst
go-tangerine-ec3db0f56c779387132dcf2049ed32bf4ed34a4f.zip
cmd/clef, signer: initial poc of the standalone signer (#16154)
* signer: introduce external signer command * cmd/signer, rpc: Implement new signer. Add info about remote user to Context * signer: refactored request/response, made use of urfave.cli * cmd/signer: Use common flags * cmd/signer: methods to validate calldata against abi * cmd/signer: work on abi parser * signer: add mutex around UI * cmd/signer: add json 4byte directory, remove passwords from api * cmd/signer: minor changes * cmd/signer: Use ErrRequestDenied, enable lightkdf * cmd/signer: implement tests * cmd/signer: made possible for UI to modify tx parameters * cmd/signer: refactors, removed channels in ui comms, added UI-api via stdin/out * cmd/signer: Made lowercase json-definitions, added UI-signer test functionality * cmd/signer: update documentation * cmd/signer: fix bugs, improve abi detection, abi argument display * cmd/signer: minor change in json format * cmd/signer: rework json communication * cmd/signer: implement mixcase addresses in API, fix json id bug * cmd/signer: rename fromaccount, update pythonpoc with new json encoding format * cmd/signer: make use of new abi interface * signer: documentation * signer/main: remove redundant option * signer: implement audit logging * signer: create package 'signer', minor changes * common: add 0x-prefix to mixcaseaddress in json marshalling + validation * signer, rules, storage: implement rules + ephemeral storage for signer rules * signer: implement OnApprovedTx, change signing response (API BREAKAGE) * signer: refactoring + documentation * signer/rules: implement dispatching to next handler * signer: docs * signer/rules: hide json-conversion from users, ensure context is cleaned * signer: docs * signer: implement validation rules, change signature of call_info * signer: fix log flaw with string pointer * signer: implement custom 4byte databsae that saves submitted signatures * signer/storage: implement aes-gcm-backed credential storage * accounts: implement json unmarshalling of url * signer: fix listresponse, fix gas->uint64 * node: make http/ipc start methods public * signer: add ipc capability+review concerns * accounts: correct docstring * signer: address review concerns * rpc: go fmt -s * signer: review concerns+ baptize Clef * signer,node: move Start-functions to separate file * signer: formatting
Diffstat (limited to 'signer/core')
-rw-r--r--signer/core/abihelper.go256
-rw-r--r--signer/core/abihelper_test.go247
-rw-r--r--signer/core/api.go500
-rw-r--r--signer/core/api_test.go386
-rw-r--r--signer/core/auditlog.go110
-rw-r--r--signer/core/cliui.go247
-rw-r--r--signer/core/stdioui.go113
-rw-r--r--signer/core/types.go95
-rw-r--r--signer/core/validation.go163
-rw-r--r--signer/core/validation_test.go139
10 files changed, 2256 insertions, 0 deletions
diff --git a/signer/core/abihelper.go b/signer/core/abihelper.go
new file mode 100644
index 000000000..2674c7346
--- /dev/null
+++ b/signer/core/abihelper.go
@@ -0,0 +1,256 @@
+// Copyright 2018 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 core
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "strings"
+
+ "github.com/ethereum/go-ethereum/accounts/abi"
+ "github.com/ethereum/go-ethereum/common"
+
+ "bytes"
+ "os"
+ "regexp"
+)
+
+type decodedArgument struct {
+ soltype abi.Argument
+ value interface{}
+}
+type decodedCallData struct {
+ signature string
+ name string
+ inputs []decodedArgument
+}
+
+// String implements stringer interface, tries to use the underlying value-type
+func (arg decodedArgument) String() string {
+ var value string
+ switch arg.value.(type) {
+ case fmt.Stringer:
+ value = arg.value.(fmt.Stringer).String()
+ default:
+ value = fmt.Sprintf("%v", arg.value)
+ }
+ return fmt.Sprintf("%v: %v", arg.soltype.Type.String(), value)
+}
+
+// String implements stringer interface for decodedCallData
+func (cd decodedCallData) String() string {
+ args := make([]string, len(cd.inputs))
+ for i, arg := range cd.inputs {
+ args[i] = arg.String()
+ }
+ return fmt.Sprintf("%s(%s)", cd.name, strings.Join(args, ","))
+}
+
+// parseCallData matches the provided call data against the abi definition,
+// and returns a struct containing the actual go-typed values
+func parseCallData(calldata []byte, abidata string) (*decodedCallData, error) {
+
+ if len(calldata) < 4 {
+ return nil, fmt.Errorf("Invalid ABI-data, incomplete method signature of (%d bytes)", len(calldata))
+ }
+
+ sigdata, argdata := calldata[:4], calldata[4:]
+ if len(argdata)%32 != 0 {
+ return nil, fmt.Errorf("Not ABI-encoded data; length should be a multiple of 32 (was %d)", len(argdata))
+ }
+
+ abispec, err := abi.JSON(strings.NewReader(abidata))
+ if err != nil {
+ return nil, fmt.Errorf("Failed parsing JSON ABI: %v, abidata: %v", err, abidata)
+ }
+
+ method, err := abispec.MethodById(sigdata)
+ if err != nil {
+ return nil, err
+ }
+
+ v, err := method.Inputs.UnpackValues(argdata)
+ if err != nil {
+ return nil, err
+ }
+
+ decoded := decodedCallData{signature: method.Sig(), name: method.Name}
+
+ for n, argument := range method.Inputs {
+ if err != nil {
+ return nil, fmt.Errorf("Failed to decode argument %d (signature %v): %v", n, method.Sig(), err)
+ } else {
+ decodedArg := decodedArgument{
+ soltype: argument,
+ value: v[n],
+ }
+ decoded.inputs = append(decoded.inputs, decodedArg)
+ }
+ }
+
+ // We're finished decoding the data. At this point, we encode the decoded data to see if it matches with the
+ // original data. If we didn't do that, it would e.g. be possible to stuff extra data into the arguments, which
+ // is not detected by merely decoding the data.
+
+ var (
+ encoded []byte
+ )
+ encoded, err = method.Inputs.PackValues(v)
+
+ if err != nil {
+ return nil, err
+ }
+
+ if !bytes.Equal(encoded, argdata) {
+ was := common.Bytes2Hex(encoded)
+ exp := common.Bytes2Hex(argdata)
+ return nil, fmt.Errorf("WARNING: Supplied data is stuffed with extra data. \nWant %s\nHave %s\nfor method %v", exp, was, method.Sig())
+ }
+ return &decoded, nil
+}
+
+// MethodSelectorToAbi converts a method selector into an ABI struct. The returned data is a valid json string
+// which can be consumed by the standard abi package.
+func MethodSelectorToAbi(selector string) ([]byte, error) {
+
+ re := regexp.MustCompile(`^([^\)]+)\(([a-z0-9,\[\]]*)\)`)
+
+ type fakeArg struct {
+ Type string `json:"type"`
+ }
+ type fakeABI struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Inputs []fakeArg `json:"inputs"`
+ }
+ groups := re.FindStringSubmatch(selector)
+ if len(groups) != 3 {
+ return nil, fmt.Errorf("Did not match: %v (%v matches)", selector, len(groups))
+ }
+ name := groups[1]
+ args := groups[2]
+ arguments := make([]fakeArg, 0)
+ if len(args) > 0 {
+ for _, arg := range strings.Split(args, ",") {
+ arguments = append(arguments, fakeArg{arg})
+ }
+ }
+ abicheat := fakeABI{
+ name, "function", arguments,
+ }
+ return json.Marshal([]fakeABI{abicheat})
+
+}
+
+type AbiDb struct {
+ db map[string]string
+ customdb map[string]string
+ customdbPath string
+}
+
+// NewEmptyAbiDB exists for test purposes
+func NewEmptyAbiDB() (*AbiDb, error) {
+ return &AbiDb{make(map[string]string), make(map[string]string), ""}, nil
+}
+
+// NewAbiDBFromFile loads signature database from file, and
+// errors if the file is not valid json. Does no other validation of contents
+func NewAbiDBFromFile(path string) (*AbiDb, error) {
+ raw, err := ioutil.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+ db, err := NewEmptyAbiDB()
+ if err != nil {
+ return nil, err
+ }
+ json.Unmarshal(raw, &db.db)
+ return db, nil
+}
+
+// NewAbiDBFromFiles loads both the standard signature database and a custom database. The latter will be used
+// to write new values into if they are submitted via the API
+func NewAbiDBFromFiles(standard, custom string) (*AbiDb, error) {
+
+ db := &AbiDb{make(map[string]string), make(map[string]string), custom}
+ db.customdbPath = custom
+
+ raw, err := ioutil.ReadFile(standard)
+ if err != nil {
+ return nil, err
+ }
+ json.Unmarshal(raw, &db.db)
+ // Custom file may not exist. Will be created during save, if needed
+ if _, err := os.Stat(custom); err == nil {
+ raw, err = ioutil.ReadFile(custom)
+ if err != nil {
+ return nil, err
+ }
+ json.Unmarshal(raw, &db.customdb)
+ }
+
+ return db, nil
+}
+
+// LookupMethodSelector checks the given 4byte-sequence against the known ABI methods.
+// OBS: This method does not validate the match, it's assumed the caller will do so
+func (db *AbiDb) LookupMethodSelector(id []byte) (string, error) {
+ if len(id) < 4 {
+ return "", fmt.Errorf("Expected 4-byte id, got %d", len(id))
+ }
+ sig := common.ToHex(id[:4])
+ if key, exists := db.db[sig]; exists {
+ return key, nil
+ }
+ if key, exists := db.customdb[sig]; exists {
+ return key, nil
+ }
+ return "", fmt.Errorf("Signature %v not found", sig)
+}
+func (db *AbiDb) Size() int {
+ return len(db.db)
+}
+
+// saveCustomAbi saves a signature ephemerally. If custom file is used, also saves to disk
+func (db *AbiDb) saveCustomAbi(selector, signature string) error {
+ db.customdb[signature] = selector
+ if db.customdbPath == "" {
+ return nil //Not an error per se, just not used
+ }
+ d, err := json.Marshal(db.customdb)
+ if err != nil {
+ return err
+ }
+ err = ioutil.WriteFile(db.customdbPath, d, 0600)
+ return err
+}
+
+// Adds a signature to the database, if custom database saving is enabled.
+// OBS: This method does _not_ validate the correctness of the data,
+// it is assumed that the caller has already done so
+func (db *AbiDb) AddSignature(selector string, data []byte) error {
+ if len(data) < 4 {
+ return nil
+ }
+ _, err := db.LookupMethodSelector(data[:4])
+ if err == nil {
+ return nil
+ }
+ sig := common.ToHex(data[:4])
+ return db.saveCustomAbi(selector, sig)
+}
diff --git a/signer/core/abihelper_test.go b/signer/core/abihelper_test.go
new file mode 100644
index 000000000..8bb577669
--- /dev/null
+++ b/signer/core/abihelper_test.go
@@ -0,0 +1,247 @@
+// Copyright 2018 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 core
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "io/ioutil"
+ "math/big"
+ "reflect"
+
+ "github.com/ethereum/go-ethereum/accounts/abi"
+ "github.com/ethereum/go-ethereum/common"
+)
+
+func verify(t *testing.T, jsondata, calldata string, exp []interface{}) {
+
+ abispec, err := abi.JSON(strings.NewReader(jsondata))
+ if err != nil {
+ t.Fatal(err)
+ }
+ cd := common.Hex2Bytes(calldata)
+ sigdata, argdata := cd[:4], cd[4:]
+ method, err := abispec.MethodById(sigdata)
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ data, err := method.Inputs.UnpackValues(argdata)
+
+ if len(data) != len(exp) {
+ t.Fatalf("Mismatched length, expected %d, got %d", len(exp), len(data))
+ }
+ for i, elem := range data {
+ if !reflect.DeepEqual(elem, exp[i]) {
+ t.Fatalf("Unpack error, arg %d, got %v, want %v", i, elem, exp[i])
+ }
+ }
+}
+func TestNewUnpacker(t *testing.T) {
+ type unpackTest struct {
+ jsondata string
+ calldata string
+ exp []interface{}
+ }
+ testcases := []unpackTest{
+ { // https://solidity.readthedocs.io/en/develop/abi-spec.html#use-of-dynamic-types
+ `[{"type":"function","name":"f", "inputs":[{"type":"uint256"},{"type":"uint32[]"},{"type":"bytes10"},{"type":"bytes"}]}]`,
+ // 0x123, [0x456, 0x789], "1234567890", "Hello, world!"
+ "8be65246" + "00000000000000000000000000000000000000000000000000000000000001230000000000000000000000000000000000000000000000000000000000000080313233343536373839300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000004560000000000000000000000000000000000000000000000000000000000000789000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20776f726c642100000000000000000000000000000000000000",
+ []interface{}{
+ big.NewInt(0x123),
+ []uint32{0x456, 0x789},
+ [10]byte{49, 50, 51, 52, 53, 54, 55, 56, 57, 48},
+ common.Hex2Bytes("48656c6c6f2c20776f726c6421"),
+ },
+ }, { // https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI#examples
+ `[{"type":"function","name":"sam","inputs":[{"type":"bytes"},{"type":"bool"},{"type":"uint256[]"}]}]`,
+ // "dave", true and [1,2,3]
+ "a5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003",
+ []interface{}{
+ []byte{0x64, 0x61, 0x76, 0x65},
+ true,
+ []*big.Int{big.NewInt(1), big.NewInt(2), big.NewInt(3)},
+ },
+ }, {
+ `[{"type":"function","name":"send","inputs":[{"type":"uint256"}]}]`,
+ "a52c101e0000000000000000000000000000000000000000000000000000000000000012",
+ []interface{}{big.NewInt(0x12)},
+ }, {
+ `[{"type":"function","name":"compareAndApprove","inputs":[{"type":"address"},{"type":"uint256"},{"type":"uint256"}]}]`,
+ "751e107900000000000000000000000000000133700000deadbeef00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001",
+ []interface{}{
+ common.HexToAddress("0x00000133700000deadbeef000000000000000000"),
+ new(big.Int).SetBytes([]byte{0x00}),
+ big.NewInt(0x1),
+ },
+ },
+ }
+ for _, c := range testcases {
+ verify(t, c.jsondata, c.calldata, c.exp)
+ }
+
+}
+
+/*
+func TestReflect(t *testing.T) {
+ a := big.NewInt(0)
+ b := new(big.Int).SetBytes([]byte{0x00})
+ if !reflect.DeepEqual(a, b) {
+ t.Fatalf("Nope, %v != %v", a, b)
+ }
+}
+*/
+
+func TestCalldataDecoding(t *testing.T) {
+
+ // send(uint256) : a52c101e
+ // compareAndApprove(address,uint256,uint256) : 751e1079
+ // issue(address[],uint256) : 42958b54
+ jsondata := `
+[
+ {"type":"function","name":"send","inputs":[{"name":"a","type":"uint256"}]},
+ {"type":"function","name":"compareAndApprove","inputs":[{"name":"a","type":"address"},{"name":"a","type":"uint256"},{"name":"a","type":"uint256"}]},
+ {"type":"function","name":"issue","inputs":[{"name":"a","type":"address[]"},{"name":"a","type":"uint256"}]},
+ {"type":"function","name":"sam","inputs":[{"name":"a","type":"bytes"},{"name":"a","type":"bool"},{"name":"a","type":"uint256[]"}]}
+]`
+ //Expected failures
+ for _, hexdata := range []string{
+ "a52c101e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000042",
+ "a52c101e000000000000000000000000000000000000000000000000000000000000001200",
+ "a52c101e00000000000000000000000000000000000000000000000000000000000000",
+ "a52c101e",
+ "a52c10",
+ "",
+ // Too short
+ "751e10790000000000000000000000000000000000000000000000000000000000000012",
+ "751e1079FFffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ //Not valid multiple of 32
+ "deadbeef00000000000000000000000000000000000000000000000000000000000000",
+ //Too short 'issue'
+ "42958b5400000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000042",
+ // Too short compareAndApprove
+ "a52c101e00ff0000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000042",
+ // From https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI
+ // contains a bool with illegal values
+ "a5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003",
+ } {
+ _, err := parseCallData(common.Hex2Bytes(hexdata), jsondata)
+ if err == nil {
+ t.Errorf("Expected decoding to fail: %s", hexdata)
+ }
+ }
+
+ //Expected success
+ for _, hexdata := range []string{
+ // From https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI
+ "a5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003",
+ "a52c101e0000000000000000000000000000000000000000000000000000000000000012",
+ "a52c101eFFffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "751e1079000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "42958b54" +
+ // start of dynamic type
+ "0000000000000000000000000000000000000000000000000000000000000040" +
+ //uint256
+ "0000000000000000000000000000000000000000000000000000000000000001" +
+ // length of array
+ "0000000000000000000000000000000000000000000000000000000000000002" +
+ // array values
+ "000000000000000000000000000000000000000000000000000000000000dead" +
+ "000000000000000000000000000000000000000000000000000000000000beef",
+ } {
+ _, err := parseCallData(common.Hex2Bytes(hexdata), jsondata)
+ if err != nil {
+ t.Errorf("Unexpected failure on input %s:\n %v (%d bytes) ", hexdata, err, len(common.Hex2Bytes(hexdata)))
+ }
+ }
+}
+
+func TestSelectorUnmarshalling(t *testing.T) {
+ var (
+ db *AbiDb
+ err error
+ abistring []byte
+ abistruct abi.ABI
+ )
+
+ db, err = NewAbiDBFromFile("../../cmd/clef/4byte.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ fmt.Printf("DB size %v\n", db.Size())
+ for id, selector := range db.db {
+
+ abistring, err = MethodSelectorToAbi(selector)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ abistruct, err = abi.JSON(strings.NewReader(string(abistring)))
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ m, err := abistruct.MethodById(common.Hex2Bytes(id[2:]))
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ if m.Sig() != selector {
+ t.Errorf("Expected equality: %v != %v", m.Sig(), selector)
+ }
+ }
+
+}
+
+func TestCustomABI(t *testing.T) {
+ d, err := ioutil.TempDir("", "signer-4byte-test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ filename := fmt.Sprintf("%s/4byte_custom.json", d)
+ abidb, err := NewAbiDBFromFiles("../../cmd/clef/4byte.json", filename)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Now we'll remove all existing signatures
+ abidb.db = make(map[string]string)
+ calldata := common.Hex2Bytes("a52c101edeadbeef")
+ _, err = abidb.LookupMethodSelector(calldata)
+ if err == nil {
+ t.Fatalf("Should not find a match on empty db")
+ }
+ if err = abidb.AddSignature("send(uint256)", calldata); err != nil {
+ t.Fatalf("Failed to save file: %v", err)
+ }
+ _, err = abidb.LookupMethodSelector(calldata)
+ if err != nil {
+ t.Fatalf("Should find a match for abi signature, got: %v", err)
+ }
+ //Check that it wrote to file
+ abidb2, err := NewAbiDBFromFile(filename)
+ if err != nil {
+ t.Fatalf("Failed to create new abidb: %v", err)
+ }
+ _, err = abidb2.LookupMethodSelector(calldata)
+ if err != nil {
+ t.Fatalf("Save failed: should find a match for abi signature after loading from disk")
+ }
+}
diff --git a/signer/core/api.go b/signer/core/api.go
new file mode 100644
index 000000000..1387041cc
--- /dev/null
+++ b/signer/core/api.go
@@ -0,0 +1,500 @@
+// Copyright 2018 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 core
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "math/big"
+ "reflect"
+
+ "github.com/ethereum/go-ethereum/accounts"
+ "github.com/ethereum/go-ethereum/accounts/keystore"
+ "github.com/ethereum/go-ethereum/accounts/usbwallet"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/internal/ethapi"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+// ExternalAPI defines the external API through which signing requests are made.
+type ExternalAPI interface {
+ // List available accounts
+ List(ctx context.Context) (Accounts, error)
+ // New request to create a new account
+ New(ctx context.Context) (accounts.Account, error)
+ // SignTransaction request to sign the specified transaction
+ SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error)
+ // Sign - request to sign the given data (plus prefix)
+ Sign(ctx context.Context, addr common.MixedcaseAddress, data hexutil.Bytes) (hexutil.Bytes, error)
+ // EcRecover - request to perform ecrecover
+ EcRecover(ctx context.Context, data, sig hexutil.Bytes) (common.Address, error)
+ // Export - request to export an account
+ Export(ctx context.Context, addr common.Address) (json.RawMessage, error)
+ // Import - request to import an account
+ Import(ctx context.Context, keyJSON json.RawMessage) (Account, error)
+}
+
+// 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 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) (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) (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)
+ // OnApprovedTx notifies the UI about a transaction having been successfully signed.
+ // This method can be used by a UI to keep track of e.g. how much has been sent to a particular recipient.
+ OnApprovedTx(tx ethapi.SignTransactionResult)
+ // OnSignerStartup is invoked when the signer boots, and tells the UI info about external API location and version
+ // information
+ OnSignerStartup(info StartupInfo)
+}
+
+// SignerAPI defines the actual implementation of ExternalAPI
+type SignerAPI struct {
+ chainID *big.Int
+ am *accounts.Manager
+ UI SignerUI
+ validator *Validator
+}
+
+// Metadata about a request
+type Metadata struct {
+ Remote string `json:"remote"`
+ Local string `json:"local"`
+ Scheme string `json:"scheme"`
+}
+
+// MetadataFromContext extracts Metadata from a given context.Context
+func MetadataFromContext(ctx context.Context) Metadata {
+ m := Metadata{"NA", "NA", "NA"} // batman
+
+ 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
+}
+
+// String implements Stringer interface
+func (m Metadata) String() string {
+ s, err := json.Marshal(m)
+ if err == nil {
+ return string(s)
+ }
+ return err.Error()
+}
+
+// types for the requests/response types between signer and UI
+type (
+ // SignTxRequest contains info about a Transaction to sign
+ SignTxRequest struct {
+ Transaction SendTxArgs `json:"transaction"`
+ Callinfo []ValidationInfo `json:"call_info"`
+ Meta Metadata `json:"meta"`
+ }
+ // SignTxResponse result from SignTxRequest
+ SignTxResponse struct {
+ //The UI may make changes to the TX
+ Transaction SendTxArgs `json:"transaction"`
+ Approved bool `json:"approved"`
+ Password string `json:"password"`
+ }
+ // ExportRequest info about query to export accounts
+ ExportRequest struct {
+ Address common.Address `json:"address"`
+ Meta Metadata `json:"meta"`
+ }
+ // ExportResponse response to export-request
+ ExportResponse struct {
+ Approved bool `json:"approved"`
+ }
+ // ImportRequest info about request to import an Account
+ ImportRequest struct {
+ Meta Metadata `json:"meta"`
+ }
+ ImportResponse struct {
+ Approved bool `json:"approved"`
+ OldPassword string `json:"old_password"`
+ NewPassword string `json:"new_password"`
+ }
+ SignDataRequest struct {
+ Address common.MixedcaseAddress `json:"address"`
+ Rawdata hexutil.Bytes `json:"raw_data"`
+ Message string `json:"message"`
+ Hash hexutil.Bytes `json:"hash"`
+ Meta Metadata `json:"meta"`
+ }
+ SignDataResponse struct {
+ Approved bool `json:"approved"`
+ Password string
+ }
+ NewAccountRequest struct {
+ Meta Metadata `json:"meta"`
+ }
+ NewAccountResponse struct {
+ Approved bool `json:"approved"`
+ Password string `json:"password"`
+ }
+ ListRequest struct {
+ Accounts []Account `json:"accounts"`
+ Meta Metadata `json:"meta"`
+ }
+ ListResponse struct {
+ Accounts []Account `json:"accounts"`
+ }
+ Message struct {
+ Text string `json:"text"`
+ }
+ StartupInfo struct {
+ Info map[string]interface{} `json:"info"`
+ }
+)
+
+var ErrRequestDenied = errors.New("Request denied")
+
+type errorWrapper struct {
+ msg string
+ err error
+}
+
+func (ew errorWrapper) String() string {
+ return fmt.Sprintf("%s\n%s", ew.msg, ew.err)
+}
+
+// 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.
+// 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 {
+ var (
+ backends []accounts.Backend
+ n, p = keystore.StandardScryptN, keystore.StandardScryptP
+ )
+ if lightKDF {
+ n, p = keystore.LightScryptN, keystore.LightScryptP
+ }
+ // support password based accounts
+ if len(ksLocation) > 0 {
+ backends = append(backends, keystore.NewKeyStore(ksLocation, n, p))
+ }
+ if !noUSB {
+ // Start a USB hub for Ledger hardware wallets
+ if ledgerhub, err := usbwallet.NewLedgerHub(); err != nil {
+ log.Warn(fmt.Sprintf("Failed to start Ledger hub, disabling: %v", err))
+ } else {
+ backends = append(backends, ledgerhub)
+ log.Debug("Ledger support enabled")
+ }
+ // Start a USB hub for Trezor hardware wallets
+ if trezorhub, err := usbwallet.NewTrezorHub(); err != nil {
+ log.Warn(fmt.Sprintf("Failed to start Trezor hub, disabling: %v", err))
+ } else {
+ backends = append(backends, trezorhub)
+ log.Debug("Trezor support enabled")
+ }
+ }
+ return &SignerAPI{big.NewInt(chainID), accounts.NewManager(backends...), ui, NewValidator(abidb)}
+}
+
+// List returns the set of wallet this signer manages. Each wallet can contain
+// multiple accounts.
+func (api *SignerAPI) List(ctx context.Context) (Accounts, error) {
+ var accs []Account
+ for _, wallet := range api.am.Wallets() {
+ for _, acc := range wallet.Accounts() {
+ acc := Account{Typ: "Account", URL: wallet.URL(), Address: acc.Address}
+ accs = append(accs, acc)
+ }
+ }
+ result, err := api.UI.ApproveListing(&ListRequest{Accounts: accs, Meta: MetadataFromContext(ctx)})
+ if err != nil {
+ return nil, err
+ }
+ if result.Accounts == nil {
+ return nil, ErrRequestDenied
+
+ }
+ return result.Accounts, nil
+}
+
+// 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) {
+ be := api.am.Backends(keystore.KeyStoreType)
+ if len(be) == 0 {
+ return accounts.Account{}, errors.New("password based accounts not supported")
+ }
+ resp, err := api.UI.ApproveNewAccount(&NewAccountRequest{MetadataFromContext(ctx)})
+
+ if err != nil {
+ return accounts.Account{}, err
+ }
+ if !resp.Approved {
+ return accounts.Account{}, ErrRequestDenied
+ }
+ return be[0].(*keystore.KeyStore).NewAccount(resp.Password)
+}
+
+// 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.Transaction.From, new.Transaction.From; !reflect.DeepEqual(f0, f1) {
+ log.Info("Sender-account changed by UI", "was", f0, "is", f1)
+ modified = true
+ }
+ if t0, t1 := original.Transaction.To, new.Transaction.To; !reflect.DeepEqual(t0, t1) {
+ log.Info("Recipient-account changed by UI", "was", t0, "is", t1)
+ modified = true
+ }
+ if g0, g1 := original.Transaction.Gas, new.Transaction.Gas; g0 != g1 {
+ 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.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.Cmp(&v1) != 0 {
+ modified = true
+ log.Info("Value changed by UI", "was", v0, "is", v1)
+ }
+ if d0, d1 := original.Transaction.Data, new.Transaction.Data; d0 != d1 {
+ d0s := ""
+ d1s := ""
+ if d0 != nil {
+ d0s = common.ToHex(*d0)
+ }
+ if d1 != nil {
+ d1s = common.ToHex(*d1)
+ }
+ if d1s != d0s {
+ modified = true
+ log.Info("Data changed by UI", "was", d0s, "is", d1s)
+ }
+ }
+ if n0, n1 := original.Transaction.Nonce, new.Transaction.Nonce; n0 != n1 {
+ modified = true
+ log.Info("Nonce changed by UI", "was", n0, "is", n1)
+ }
+ return modified
+}
+
+// SignTransaction signs the given Transaction and returns it both as json and rlp-encoded form
+func (api *SignerAPI) SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) {
+ var (
+ err error
+ result SignTxResponse
+ )
+ msgs, err := api.validator.ValidateTransaction(&args, methodSelector)
+ if err != nil {
+ return nil, err
+ }
+
+ req := SignTxRequest{
+ Transaction: args,
+ Meta: MetadataFromContext(ctx),
+ Callinfo: msgs.Messages,
+ }
+ // Process approval
+ 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)
+ var (
+ acc accounts.Account
+ wallet accounts.Wallet
+ )
+ acc = accounts.Account{Address: result.Transaction.From.Address()}
+ wallet, err = api.am.Find(acc)
+ if err != nil {
+ return nil, err
+ }
+ // Convert fields into a real transaction
+ var unsignedTx = result.Transaction.toTransaction()
+
+ // The one to sign is the one that was returned from the UI
+ signedTx, err := wallet.SignTxWithPassphrase(acc, result.Password, unsignedTx, api.chainID)
+ if err != nil {
+ api.UI.ShowError(err.Error())
+ return nil, err
+ }
+
+ rlpdata, err := rlp.EncodeToBytes(signedTx)
+ response := ethapi.SignTransactionResult{Raw: rlpdata, Tx: signedTx}
+
+ // Finally, send the signed tx to the UI
+ api.UI.OnApprovedTx(response)
+ // ...and to the external caller
+ return &response, nil
+
+}
+
+// Sign calculates an Ethereum ECDSA signature for:
+// keccack256("\x19Ethereum Signed Message:\n" + len(message) + message))
+//
+// Note, the produced signature conforms to the secp256k1 curve R, S and V values,
+// where the V value will be 27 or 28 for legacy reasons.
+//
+// The key used to calculate the signature is decrypted with the given password.
+//
+// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign
+func (api *SignerAPI) Sign(ctx context.Context, addr common.MixedcaseAddress, 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: MetadataFromContext(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.Address()}
+ wallet, err := api.am.Find(account)
+ if err != nil {
+ return nil, err
+ }
+ // 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
+ }
+ 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.
+// 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})
+// addr = ecrecover(hash, signature)
+//
+// Note, the signature must conform to the secp256k1 curve R, S and V values, where
+// the V value must be be 27 or 28 for legacy reasons.
+//
+// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_ecRecover
+func (api *SignerAPI) EcRecover(ctx context.Context, data, sig hexutil.Bytes) (common.Address, error) {
+ if len(sig) != 65 {
+ return common.Address{}, fmt.Errorf("signature must be 65 bytes long")
+ }
+ if sig[64] != 27 && sig[64] != 28 {
+ return common.Address{}, fmt.Errorf("invalid Ethereum signature (V is not 27 or 28)")
+ }
+ sig[64] -= 27 // Transform yellow paper V from 27/28 to 0/1
+ hash, _ := SignHash(data)
+ rpk, err := crypto.Ecrecover(hash, sig)
+ if err != nil {
+ return common.Address{}, err
+ }
+ pubKey := crypto.ToECDSAPub(rpk)
+ recoveredAddr := crypto.PubkeyToAddress(*pubKey)
+ return recoveredAddr, nil
+}
+
+// SignHash is a helper function that calculates a hash for the given message that can be
+// safely used to calculate a signature from.
+//
+// The hash is calculated as
+// keccak256("\x19Ethereum Signed Message:\n"${message length}${message}).
+//
+// This gives context to the signed message and prevents signing of transactions.
+func SignHash(data []byte) ([]byte, string) {
+ msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data)
+ return crypto.Keccak256([]byte(msg)), msg
+}
+
+// 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) {
+ res, err := api.UI.ApproveExport(&ExportRequest{Address: addr, Meta: MetadataFromContext(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
+ }
+ if wallet.URL().Scheme != keystore.KeyStoreScheme {
+ return nil, fmt.Errorf("Account is not a keystore-account")
+ }
+ return ioutil.ReadFile(wallet.URL().Path)
+}
+
+// Imports tries to import the given keyJSON in the local keystore. The keyJSON data is expected to be
+// in web3 keystore format. It will decrypt the keyJSON with the given passphrase and on successful
+// 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) (Account, error) {
+ be := api.am.Backends(keystore.KeyStoreType)
+
+ if len(be) == 0 {
+ return Account{}, errors.New("password based accounts not supported")
+ }
+ res, err := api.UI.ApproveImport(&ImportRequest{Meta: MetadataFromContext(ctx)})
+
+ if err != nil {
+ return Account{}, err
+ }
+ 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/signer/core/api_test.go b/signer/core/api_test.go
new file mode 100644
index 000000000..50ad02198
--- /dev/null
+++ b/signer/core/api_test.go
@@ -0,0 +1,386 @@
+// Copyright 2018 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 core
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io/ioutil"
+ "math/big"
+ "os"
+ "path/filepath"
+ "testing"
+ "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/internal/ethapi"
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+//Used for testing
+type HeadlessUI struct {
+ controller chan string
+}
+
+func (ui *HeadlessUI) OnSignerStartup(info StartupInfo) {
+}
+
+func (ui *HeadlessUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
+ fmt.Printf("OnApproved called")
+}
+
+func (ui *HeadlessUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) {
+
+ switch <-ui.controller {
+ case "Y":
+ return SignTxResponse{request.Transaction, true, <-ui.controller}, nil
+ case "M": //Modify
+ 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, true, <-ui.controller}, nil
+ default:
+ return SignTxResponse{request.Transaction, false, ""}, nil
+ }
+}
+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) (ExportResponse, error) {
+
+ return ExportResponse{<-ui.controller == "Y"}, nil
+
+}
+func (ui *HeadlessUI) ApproveImport(request *ImportRequest) (ImportResponse, error) {
+
+ if "Y" == <-ui.controller {
+ return ImportResponse{true, <-ui.controller, <-ui.controller}, nil
+ }
+ return ImportResponse{false, "", ""}, nil
+}
+func (ui *HeadlessUI) ApproveListing(request *ListRequest) (ListResponse, error) {
+
+ switch <-ui.controller {
+ case "A":
+ return ListResponse{request.Accounts}, nil
+ case "1":
+ l := make([]Account, 1)
+ l[0] = request.Accounts[1]
+ return ListResponse{l}, nil
+ default:
+ return ListResponse{nil}, nil
+ }
+}
+func (ui *HeadlessUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) {
+
+ 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
+ fmt.Fprint(os.Stderr, message)
+}
+func (ui *HeadlessUI) ShowInfo(message string) {
+ //stdout is used by communication
+ fmt.Fprint(os.Stderr, message)
+}
+
+func tmpDirName(t *testing.T) string {
+ d, err := ioutil.TempDir("", "eth-keystore-test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ d, err = filepath.EvalSymlinks(d)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return d
+}
+
+func setup(t *testing.T) (*SignerAPI, chan string) {
+
+ controller := make(chan string, 10)
+
+ db, err := NewAbiDBFromFile("../../cmd/clef/4byte.json")
+ if err != nil {
+ utils.Fatalf(err.Error())
+ }
+ var (
+ ui = &HeadlessUI{controller}
+ api = NewSignerAPI(
+ 1,
+ tmpDirName(t),
+ true,
+ ui,
+ db,
+ true)
+ )
+ return api, controller
+}
+func createAccount(control chan string, api *SignerAPI, t *testing.T) {
+
+ control <- "Y"
+ control <- "apassword"
+ _, err := api.New(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Some time to allow changes to propagate
+ time.Sleep(250 * time.Millisecond)
+}
+func failCreateAccount(control chan string, api *SignerAPI, t *testing.T) {
+ control <- "N"
+ acc, err := api.New(context.Background())
+ if err != ErrRequestDenied {
+ t.Fatal(err)
+ }
+ if acc.Address != (common.Address{}) {
+ t.Fatal("Empty address should be returned")
+ }
+}
+func list(control chan string, api *SignerAPI, t *testing.T) []Account {
+ control <- "A"
+ list, err := api.List(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ return list
+}
+
+func TestNewAcc(t *testing.T) {
+
+ api, control := setup(t)
+ verifyNum := func(num int) {
+ if list := list(control, api, t); len(list) != num {
+ t.Errorf("Expected %d accounts, got %d", num, len(list))
+ }
+ }
+ // Testing create and create-deny
+ createAccount(control, api, t)
+ createAccount(control, api, t)
+ failCreateAccount(control, api, t)
+ failCreateAccount(control, api, t)
+ createAccount(control, api, t)
+ failCreateAccount(control, api, t)
+ createAccount(control, api, t)
+ failCreateAccount(control, api, t)
+ verifyNum(4)
+
+ // Testing listing:
+ // 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")
+ }
+ // Listing denied
+ control <- "Nope"
+ list, err = api.List(context.Background())
+ if len(list) != 0 {
+ t.Fatalf("List should be empty")
+ }
+ if err != ErrRequestDenied {
+ t.Fatal("Expected deny")
+ }
+}
+
+func TestSignData(t *testing.T) {
+
+ api, control := setup(t)
+ //Create two accounts
+ createAccount(control, api, t)
+ createAccount(control, api, t)
+ control <- "1"
+ list, err := api.List(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ a := common.NewMixedcaseAddress(list[0].Address)
+
+ control <- "Y"
+ control <- "wrongpassword"
+ h, err := api.Sign(context.Background(), a, []byte("EHLO world"))
+ if h != nil {
+ t.Errorf("Expected nil-data, got %x", h)
+ }
+ if err != keystore.ErrDecrypt {
+ t.Errorf("Expected ErrLocked! %v", err)
+ }
+
+ control <- "No way"
+ h, err = api.Sign(context.Background(), a, []byte("EHLO world"))
+ if h != nil {
+ t.Errorf("Expected nil-data, got %x", h)
+ }
+ if err != ErrRequestDenied {
+ t.Errorf("Expected ErrRequestDenied! %v", err)
+ }
+
+ control <- "Y"
+ control <- "apassword"
+ h, err = api.Sign(context.Background(), a, []byte("EHLO world"))
+
+ if err != nil {
+ t.Fatal(err)
+ }
+ if h == nil || len(h) != 65 {
+ t.Errorf("Expected 65 byte signature (got %d bytes)", len(h))
+ }
+}
+func mkTestTx(from common.MixedcaseAddress) SendTxArgs {
+ to := common.NewMixedcaseAddress(common.HexToAddress("0x1337"))
+ gas := hexutil.Uint64(21000)
+ gasPrice := (hexutil.Big)(*big.NewInt(2000000000))
+ value := (hexutil.Big)(*big.NewInt(1e18))
+ nonce := (hexutil.Uint64)(0)
+ data := hexutil.Bytes(common.Hex2Bytes("01020304050607080a"))
+ tx := SendTxArgs{
+ From: from,
+ To: &to,
+ Gas: gas,
+ GasPrice: gasPrice,
+ Value: value,
+ Data: &data,
+ Nonce: nonce}
+ return tx
+}
+
+func TestSignTx(t *testing.T) {
+
+ var (
+ list Accounts
+ res, res2 *ethapi.SignTransactionResult
+ err error
+ )
+
+ api, control := setup(t)
+ createAccount(control, api, t)
+ control <- "A"
+ list, err = api.List(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ a := common.NewMixedcaseAddress(list[0].Address)
+
+ methodSig := "test(uint)"
+ tx := mkTestTx(a)
+
+ control <- "Y"
+ control <- "wrongpassword"
+ res, err = api.SignTransaction(context.Background(), tx, &methodSig)
+ if res != nil {
+ t.Errorf("Expected nil-response, got %v", res)
+ }
+ if err != keystore.ErrDecrypt {
+ t.Errorf("Expected ErrLocked! %v", err)
+ }
+
+ control <- "No way"
+ res, err = api.SignTransaction(context.Background(), tx, &methodSig)
+ if res != nil {
+ t.Errorf("Expected nil-response, got %v", res)
+ }
+ if err != ErrRequestDenied {
+ t.Errorf("Expected ErrRequestDenied! %v", err)
+ }
+
+ control <- "Y"
+ control <- "apassword"
+ res, err = api.SignTransaction(context.Background(), tx, &methodSig)
+
+ if err != nil {
+ t.Fatal(err)
+ }
+ parsedTx := &types.Transaction{}
+ rlp.Decode(bytes.NewReader(res.Raw), parsedTx)
+ //The tx should NOT be modified by the UI
+ if parsedTx.Value().Cmp(tx.Value.ToInt()) != 0 {
+ t.Errorf("Expected value to be unchanged, expected %v got %v", tx.Value, parsedTx.Value())
+ }
+ control <- "Y"
+ control <- "apassword"
+
+ res2, err = api.SignTransaction(context.Background(), tx, &methodSig)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(res.Raw, res2.Raw) {
+ t.Error("Expected tx to be unmodified by UI")
+ }
+
+ //The tx is modified by the UI
+ control <- "M"
+ control <- "apassword"
+
+ res2, err = api.SignTransaction(context.Background(), tx, &methodSig)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ parsedTx2 := &types.Transaction{}
+ rlp.Decode(bytes.NewReader(res.Raw), parsedTx2)
+ //The tx should be modified by the UI
+ if parsedTx2.Value().Cmp(tx.Value.ToInt()) != 0 {
+ t.Errorf("Expected value to be unchanged, got %v", parsedTx.Value())
+ }
+
+ if bytes.Equal(res.Raw, res2.Raw) {
+ t.Error("Expected tx to be modified by UI")
+ }
+
+}
+
+/*
+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/signer/core/auditlog.go b/signer/core/auditlog.go
new file mode 100644
index 000000000..d0ba733d2
--- /dev/null
+++ b/signer/core/auditlog.go
@@ -0,0 +1,110 @@
+// Copyright 2018 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 core
+
+import (
+ "context"
+
+ "encoding/json"
+
+ "github.com/ethereum/go-ethereum/accounts"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/internal/ethapi"
+ "github.com/ethereum/go-ethereum/log"
+)
+
+type AuditLogger struct {
+ log log.Logger
+ api ExternalAPI
+}
+
+func (l *AuditLogger) List(ctx context.Context) (Accounts, error) {
+ l.log.Info("List", "type", "request", "metadata", MetadataFromContext(ctx).String())
+ res, e := l.api.List(ctx)
+
+ l.log.Info("List", "type", "response", "data", res.String())
+
+ return res, e
+}
+
+func (l *AuditLogger) New(ctx context.Context) (accounts.Account, error) {
+ return l.api.New(ctx)
+}
+
+func (l *AuditLogger) SignTransaction(ctx context.Context, args SendTxArgs, methodSelector *string) (*ethapi.SignTransactionResult, error) {
+ sel := "<nil>"
+ if methodSelector != nil {
+ sel = *methodSelector
+ }
+ l.log.Info("SignTransaction", "type", "request", "metadata", MetadataFromContext(ctx).String(),
+ "tx", args.String(),
+ "methodSelector", sel)
+
+ res, e := l.api.SignTransaction(ctx, args, methodSelector)
+ if res != nil {
+ l.log.Info("SignTransaction", "type", "response", "data", common.Bytes2Hex(res.Raw), "error", e)
+ } else {
+ l.log.Info("SignTransaction", "type", "response", "data", res, "error", e)
+ }
+ return res, e
+}
+
+func (l *AuditLogger) Sign(ctx context.Context, addr common.MixedcaseAddress, data hexutil.Bytes) (hexutil.Bytes, error) {
+ l.log.Info("Sign", "type", "request", "metadata", MetadataFromContext(ctx).String(),
+ "addr", addr.String(), "data", common.Bytes2Hex(data))
+ b, e := l.api.Sign(ctx, addr, data)
+ l.log.Info("Sign", "type", "response", "data", common.Bytes2Hex(b), "error", e)
+ return b, e
+}
+
+func (l *AuditLogger) EcRecover(ctx context.Context, data, sig hexutil.Bytes) (common.Address, error) {
+ l.log.Info("EcRecover", "type", "request", "metadata", MetadataFromContext(ctx).String(),
+ "data", common.Bytes2Hex(data))
+ a, e := l.api.EcRecover(ctx, data, sig)
+ l.log.Info("EcRecover", "type", "response", "addr", a.String(), "error", e)
+ return a, e
+}
+
+func (l *AuditLogger) Export(ctx context.Context, addr common.Address) (json.RawMessage, error) {
+ l.log.Info("Export", "type", "request", "metadata", MetadataFromContext(ctx).String(),
+ "addr", addr.Hex())
+ j, e := l.api.Export(ctx, addr)
+ // In this case, we don't actually log the json-response, which may be extra sensitive
+ l.log.Info("Export", "type", "response", "json response size", len(j), "error", e)
+ return j, e
+}
+
+func (l *AuditLogger) Import(ctx context.Context, keyJSON json.RawMessage) (Account, error) {
+ // Don't actually log the json contents
+ l.log.Info("Import", "type", "request", "metadata", MetadataFromContext(ctx).String(),
+ "keyJSON size", len(keyJSON))
+ a, e := l.api.Import(ctx, keyJSON)
+ l.log.Info("Import", "type", "response", "addr", a.String(), "error", e)
+ return a, e
+}
+
+func NewAuditLogger(path string, api ExternalAPI) (*AuditLogger, error) {
+ l := log.New("api", "signer")
+ handler, err := log.FileHandler(path, log.LogfmtFormat())
+ if err != nil {
+ return nil, err
+ }
+ l.SetHandler(handler)
+ l.Info("Configured", "audit log", path)
+ return &AuditLogger{l, api}, nil
+}
diff --git a/signer/core/cliui.go b/signer/core/cliui.go
new file mode 100644
index 000000000..0d9b5f3d3
--- /dev/null
+++ b/signer/core/cliui.go
@@ -0,0 +1,247 @@
+// Copyright 2018 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 core
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "strings"
+
+ "sync"
+
+ "github.com/davecgh/go-spew/spew"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/internal/ethapi"
+ "github.com/ethereum/go-ethereum/log"
+ "golang.org/x/crypto/ssh/terminal"
+)
+
+type CommandlineUI struct {
+ in *bufio.Reader
+ mu sync.Mutex
+}
+
+func NewCommandlineUI() *CommandlineUI {
+ return &CommandlineUI{in: bufio.NewReader(os.Stdin)}
+}
+
+// readString reads a single line from stdin, trimming if from spaces, enforcing
+// non-emptyness.
+func (ui *CommandlineUI) readString() string {
+ for {
+ fmt.Printf("> ")
+ text, err := ui.in.ReadString('\n')
+ if err != nil {
+ log.Crit("Failed to read user input", "err", err)
+ }
+ if text = strings.TrimSpace(text); text != "" {
+ return text
+ }
+ }
+}
+
+// readPassword reads a single line from stdin, trimming it from the trailing new
+// line and returns it. The input will not be echoed.
+func (ui *CommandlineUI) readPassword() string {
+ fmt.Printf("Enter password to approve:\n")
+ fmt.Printf("> ")
+
+ 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)
+}
+
+// readPassword reads a single line from stdin, trimming it from the trailing new
+// line and returns it. The input will not be echoed.
+func (ui *CommandlineUI) readPasswordText(inputstring string) string {
+ fmt.Printf("Enter %s:\n", inputstring)
+ fmt.Printf("> ")
+ text, err := terminal.ReadPassword(int(os.Stdin.Fd()))
+ if err != nil {
+ log.Crit("Failed to read password", "err", err)
+ }
+ fmt.Println("-----------------------")
+ return string(text)
+}
+
+// confirm returns true if user enters 'Yes', otherwise false
+func (ui *CommandlineUI) confirm() bool {
+ fmt.Printf("Approve? [y/N]:\n")
+ if ui.readString() == "y" {
+ return true
+ }
+ fmt.Println("-----------------------")
+ return false
+}
+
+func showMetadata(metadata Metadata) {
+ fmt.Printf("Request context:\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) (SignTxResponse, error) {
+ ui.mu.Lock()
+ defer ui.mu.Unlock()
+ weival := request.Transaction.Value.ToInt()
+ fmt.Printf("--------- Transaction request-------------\n")
+ if to := request.Transaction.To; to != nil {
+ 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.Transaction.From.String())
+ fmt.Printf("value: %v wei\n", weival)
+ if request.Transaction.Data != nil {
+ d := *request.Transaction.Data
+ if len(d) > 0 {
+ fmt.Printf("data: %v\n", common.Bytes2Hex(d))
+ }
+ }
+ if request.Callinfo != nil {
+ fmt.Printf("\nTransaction validation:\n")
+ for _, m := range request.Callinfo {
+ fmt.Printf(" * %s : %s", m.Typ, m.Message)
+ }
+ fmt.Println()
+
+ }
+ fmt.Printf("\n")
+ showMetadata(request.Meta)
+ fmt.Printf("-------------------------------------------\n")
+ if !ui.confirm() {
+ return SignTxResponse{request.Transaction, false, ""}, nil
+ }
+ return SignTxResponse{request.Transaction, true, ui.readPassword()}, nil
+}
+
+// ApproveSignData prompt the user for confirmation to request to sign data
+func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) {
+ ui.mu.Lock()
+ defer ui.mu.Unlock()
+
+ fmt.Printf("-------- Sign data request--------------\n")
+ fmt.Printf("Account: %s\n", request.Address.String())
+ fmt.Printf("message: \n%q\n", request.Message)
+ fmt.Printf("raw data: \n%v\n", request.Rawdata)
+ fmt.Printf("message hash: %v\n", request.Hash)
+ fmt.Printf("-------------------------------------------\n")
+ showMetadata(request.Meta)
+ if !ui.confirm() {
+ return SignDataResponse{false, ""}, nil
+ }
+ return SignDataResponse{true, ui.readPassword()}, nil
+}
+
+// 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("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.Address)
+ //fmt.Printf("keyfile: \n%v\n", request.file)
+ fmt.Printf("-------------------------------------------\n")
+ showMetadata(request.Meta)
+ return ExportResponse{ui.confirm()}, nil
+}
+
+// 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("-------- Import Account request--------------\n")
+ fmt.Printf("A request has been made to import an encrypted keyfile\n")
+ fmt.Printf("-------------------------------------------\n")
+ 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) (ListResponse, error) {
+
+ ui.mu.Lock()
+ defer ui.mu.Unlock()
+
+ 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(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(request *NewAccountRequest) (NewAccountResponse, error) {
+
+ ui.mu.Lock()
+ defer ui.mu.Unlock()
+
+ 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(request.Meta)
+ if !ui.confirm() {
+ return NewAccountResponse{false, ""}, nil
+ }
+ return NewAccountResponse{true, ui.readPassword()}, nil
+}
+
+// ShowError displays error message to user
+func (ui *CommandlineUI) ShowError(message string) {
+
+ fmt.Printf("ERROR: %v\n", message)
+}
+
+// ShowInfo displays info message to user
+func (ui *CommandlineUI) ShowInfo(message string) {
+ fmt.Printf("Info: %v\n", message)
+}
+
+func (ui *CommandlineUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
+ fmt.Printf("Transaction signed:\n ")
+ spew.Dump(tx.Tx)
+}
+
+func (ui *CommandlineUI) OnSignerStartup(info StartupInfo) {
+
+ fmt.Printf("------- Signer info -------\n")
+ for k, v := range info.Info {
+ fmt.Printf("* %v : %v\n", k, v)
+ }
+}
diff --git a/signer/core/stdioui.go b/signer/core/stdioui.go
new file mode 100644
index 000000000..5640ed03b
--- /dev/null
+++ b/signer/core/stdioui.go
@@ -0,0 +1,113 @@
+// Copyright 2018 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 core
+
+import (
+ "context"
+ "sync"
+
+ "github.com/ethereum/go-ethereum/internal/ethapi"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/rpc"
+)
+
+type StdIOUI struct {
+ client rpc.Client
+ mu sync.Mutex
+}
+
+func NewStdIOUI() *StdIOUI {
+ log.Info("NewStdIOUI")
+ client, err := rpc.DialContext(context.Background(), "stdio://")
+ if err != nil {
+ log.Crit("Could not create stdio client", "err", err)
+ }
+ return &StdIOUI{client: *client}
+}
+
+// dispatch sends a request over the stdio
+func (ui *StdIOUI) dispatch(serviceMethod string, args interface{}, reply interface{}) error {
+ err := ui.client.Call(&reply, serviceMethod, args)
+ if err != nil {
+ log.Info("Error", "exc", err.Error())
+ }
+ return err
+}
+
+func (ui *StdIOUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) {
+ var result SignTxResponse
+ err := ui.dispatch("ApproveTx", request, &result)
+ return result, err
+}
+
+func (ui *StdIOUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) {
+ var result SignDataResponse
+ err := ui.dispatch("ApproveSignData", request, &result)
+ return result, err
+}
+
+func (ui *StdIOUI) ApproveExport(request *ExportRequest) (ExportResponse, error) {
+ var result ExportResponse
+ err := ui.dispatch("ApproveExport", request, &result)
+ return result, err
+}
+
+func (ui *StdIOUI) ApproveImport(request *ImportRequest) (ImportResponse, error) {
+ var result ImportResponse
+ err := ui.dispatch("ApproveImport", request, &result)
+ return result, err
+}
+
+func (ui *StdIOUI) ApproveListing(request *ListRequest) (ListResponse, error) {
+ var result ListResponse
+ err := ui.dispatch("ApproveListing", request, &result)
+ return result, err
+}
+
+func (ui *StdIOUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) {
+ var result NewAccountResponse
+ err := ui.dispatch("ApproveNewAccount", request, &result)
+ return result, err
+}
+
+func (ui *StdIOUI) ShowError(message string) {
+ err := ui.dispatch("ShowError", &Message{message}, nil)
+ if err != nil {
+ log.Info("Error calling 'ShowError'", "exc", err.Error(), "msg", message)
+ }
+}
+
+func (ui *StdIOUI) ShowInfo(message string) {
+ err := ui.dispatch("ShowInfo", Message{message}, nil)
+ if err != nil {
+ log.Info("Error calling 'ShowInfo'", "exc", err.Error(), "msg", message)
+ }
+}
+func (ui *StdIOUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
+ err := ui.dispatch("OnApprovedTx", tx, nil)
+ if err != nil {
+ log.Info("Error calling 'OnApprovedTx'", "exc", err.Error(), "tx", tx)
+ }
+}
+
+func (ui *StdIOUI) OnSignerStartup(info StartupInfo) {
+ err := ui.dispatch("OnSignerStartup", info, nil)
+ if err != nil {
+ log.Info("Error calling 'OnSignerStartup'", "exc", err.Error(), "info", info)
+ }
+}
diff --git a/signer/core/types.go b/signer/core/types.go
new file mode 100644
index 000000000..8386bd44e
--- /dev/null
+++ b/signer/core/types.go
@@ -0,0 +1,95 @@
+// Copyright 2018 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 core
+
+import (
+ "encoding/json"
+ "strings"
+
+ "math/big"
+
+ "github.com/ethereum/go-ethereum/accounts"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/core/types"
+)
+
+type Accounts []Account
+
+func (as Accounts) String() string {
+ var output []string
+ for _, a := range as {
+ output = append(output, a.String())
+ }
+ return strings.Join(output, "\n")
+}
+
+type Account struct {
+ Typ string `json:"type"`
+ URL accounts.URL `json:"url"`
+ Address common.Address `json:"address"`
+}
+
+func (a Account) String() string {
+ s, err := json.Marshal(a)
+ if err == nil {
+ return string(s)
+ }
+ return err.Error()
+}
+
+type ValidationInfo struct {
+ Typ string `json:"type"`
+ Message string `json:"message"`
+}
+type ValidationMessages struct {
+ Messages []ValidationInfo
+}
+
+// SendTxArgs represents the arguments to submit a transaction
+type SendTxArgs struct {
+ From common.MixedcaseAddress `json:"from"`
+ To *common.MixedcaseAddress `json:"to"`
+ Gas hexutil.Uint64 `json:"gas"`
+ GasPrice hexutil.Big `json:"gasPrice"`
+ Value hexutil.Big `json:"value"`
+ Nonce hexutil.Uint64 `json:"nonce"`
+ // We accept "data" and "input" for backwards-compatibility reasons.
+ Data *hexutil.Bytes `json:"data"`
+ Input *hexutil.Bytes `json:"input"`
+}
+
+func (t SendTxArgs) String() string {
+ s, err := json.Marshal(t)
+ if err == nil {
+ return string(s)
+ }
+ return err.Error()
+}
+
+func (args *SendTxArgs) toTransaction() *types.Transaction {
+ var input []byte
+ if args.Data != nil {
+ input = *args.Data
+ } else if args.Input != nil {
+ input = *args.Input
+ }
+ if args.To == nil {
+ return types.NewContractCreation(uint64(args.Nonce), (*big.Int)(&args.Value), uint64(args.Gas), (*big.Int)(&args.GasPrice), input)
+ }
+ return types.NewTransaction(uint64(args.Nonce), args.To.Address(), (*big.Int)(&args.Value), (uint64)(args.Gas), (*big.Int)(&args.GasPrice), input)
+}
diff --git a/signer/core/validation.go b/signer/core/validation.go
new file mode 100644
index 000000000..97bb3b685
--- /dev/null
+++ b/signer/core/validation.go
@@ -0,0 +1,163 @@
+// Copyright 2018 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 core
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "math/big"
+
+ "github.com/ethereum/go-ethereum/common"
+)
+
+// The validation package contains validation checks for transactions
+// - ABI-data validation
+// - Transaction semantics validation
+// The package provides warnings for typical pitfalls
+
+func (vs *ValidationMessages) crit(msg string) {
+ vs.Messages = append(vs.Messages, ValidationInfo{"CRITICAL", msg})
+}
+func (vs *ValidationMessages) warn(msg string) {
+ vs.Messages = append(vs.Messages, ValidationInfo{"WARNING", msg})
+}
+func (vs *ValidationMessages) info(msg string) {
+ vs.Messages = append(vs.Messages, ValidationInfo{"Info", msg})
+}
+
+type Validator struct {
+ db *AbiDb
+}
+
+func NewValidator(db *AbiDb) *Validator {
+ return &Validator{db}
+}
+func testSelector(selector string, data []byte) (*decodedCallData, error) {
+ if selector == "" {
+ return nil, fmt.Errorf("selector not found")
+ }
+ abiData, err := MethodSelectorToAbi(selector)
+ if err != nil {
+ return nil, err
+ }
+ info, err := parseCallData(data, string(abiData))
+ if err != nil {
+ return nil, err
+ }
+ return info, nil
+
+}
+
+// validateCallData checks if the ABI-data + methodselector (if given) can be parsed and seems to match
+func (v *Validator) validateCallData(msgs *ValidationMessages, data []byte, methodSelector *string) {
+ if len(data) == 0 {
+ return
+ }
+ if len(data) < 4 {
+ msgs.warn("Tx contains data which is not valid ABI")
+ return
+ }
+ var (
+ info *decodedCallData
+ err error
+ )
+ // Check the provided one
+ if methodSelector != nil {
+ info, err = testSelector(*methodSelector, data)
+ if err != nil {
+ msgs.warn(fmt.Sprintf("Tx contains data, but provided ABI signature could not be matched: %v", err))
+ } else {
+ msgs.info(info.String())
+ //Successfull match. add to db if not there already (ignore errors there)
+ v.db.AddSignature(*methodSelector, data[:4])
+ }
+ return
+ }
+ // Check the db
+ selector, err := v.db.LookupMethodSelector(data[:4])
+ if err != nil {
+ msgs.warn(fmt.Sprintf("Tx contains data, but the ABI signature could not be found: %v", err))
+ return
+ }
+ info, err = testSelector(selector, data)
+ if err != nil {
+ msgs.warn(fmt.Sprintf("Tx contains data, but provided ABI signature could not be matched: %v", err))
+ } else {
+ msgs.info(info.String())
+ }
+}
+
+// validateSemantics checks if the transactions 'makes sense', and generate warnings for a couple of typical scenarios
+func (v *Validator) validate(msgs *ValidationMessages, txargs *SendTxArgs, methodSelector *string) error {
+ // Prevent accidental erroneous usage of both 'input' and 'data'
+ if txargs.Data != nil && txargs.Input != nil && !bytes.Equal(*txargs.Data, *txargs.Input) {
+ // This is a showstopper
+ return errors.New(`Ambiguous request: both "data" and "input" are set and are not identical`)
+ }
+ var (
+ data []byte
+ )
+ // Place data on 'data', and nil 'input'
+ if txargs.Input != nil {
+ txargs.Data = txargs.Input
+ txargs.Input = nil
+ }
+ if txargs.Data != nil {
+ data = *txargs.Data
+ }
+
+ if txargs.To == nil {
+ //Contract creation should contain sufficient data to deploy a contract
+ // A typical error is omitting sender due to some quirk in the javascript call
+ // e.g. https://github.com/ethereum/go-ethereum/issues/16106
+ if len(data) == 0 {
+ if txargs.Value.ToInt().Cmp(big.NewInt(0)) > 0 {
+ // Sending ether into black hole
+ return errors.New(`Tx will create contract with value but empty code!`)
+ }
+ // No value submitted at least
+ msgs.crit("Tx will create contract with empty code!")
+ } else if len(data) < 40 { //Arbitrary limit
+ msgs.warn(fmt.Sprintf("Tx will will create contract, but payload is suspiciously small (%d b)", len(data)))
+ }
+ // methodSelector should be nil for contract creation
+ if methodSelector != nil {
+ msgs.warn("Tx will create contract, but method selector supplied; indicating intent to call a method.")
+ }
+
+ } else {
+ if !txargs.To.ValidChecksum() {
+ msgs.warn("Invalid checksum on to-address")
+ }
+ // Normal transaction
+ if bytes.Equal(txargs.To.Address().Bytes(), common.Address{}.Bytes()) {
+ // Sending to 0
+ msgs.crit("Tx destination is the zero address!")
+ }
+ // Validate calldata
+ v.validateCallData(msgs, data, methodSelector)
+ }
+ return nil
+}
+
+// ValidateTransaction does a number of checks on the supplied transaction, and returns either a list of warnings,
+// or an error, indicating that the transaction should be immediately rejected
+func (v *Validator) ValidateTransaction(txArgs *SendTxArgs, methodSelector *string) (*ValidationMessages, error) {
+ msgs := &ValidationMessages{}
+ return msgs, v.validate(msgs, txArgs, methodSelector)
+}
diff --git a/signer/core/validation_test.go b/signer/core/validation_test.go
new file mode 100644
index 000000000..2b33a8630
--- /dev/null
+++ b/signer/core/validation_test.go
@@ -0,0 +1,139 @@
+// Copyright 2018 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 core
+
+import (
+ "fmt"
+ "math/big"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+)
+
+func hexAddr(a string) common.Address { return common.BytesToAddress(common.FromHex(a)) }
+func mixAddr(a string) (*common.MixedcaseAddress, error) {
+ return common.NewMixedcaseAddressFromString(a)
+}
+func toHexBig(h string) hexutil.Big {
+ b := big.NewInt(0).SetBytes(common.FromHex(h))
+ return hexutil.Big(*b)
+}
+func toHexUint(h string) hexutil.Uint64 {
+ b := big.NewInt(0).SetBytes(common.FromHex(h))
+ return hexutil.Uint64(b.Uint64())
+}
+func dummyTxArgs(t txtestcase) *SendTxArgs {
+ to, _ := mixAddr(t.to)
+ from, _ := mixAddr(t.from)
+ n := toHexUint(t.n)
+ gas := toHexUint(t.g)
+ gasPrice := toHexBig(t.gp)
+ value := toHexBig(t.value)
+ var (
+ data, input *hexutil.Bytes
+ )
+ if t.d != "" {
+ a := hexutil.Bytes(common.FromHex(t.d))
+ data = &a
+ }
+ if t.i != "" {
+ a := hexutil.Bytes(common.FromHex(t.i))
+ input = &a
+
+ }
+ return &SendTxArgs{
+ From: *from,
+ To: to,
+ Value: value,
+ Nonce: n,
+ GasPrice: gasPrice,
+ Gas: gas,
+ Data: data,
+ Input: input,
+ }
+}
+
+type txtestcase struct {
+ from, to, n, g, gp, value, d, i string
+ expectErr bool
+ numMessages int
+}
+
+func TestValidator(t *testing.T) {
+ var (
+ // use empty db, there are other tests for the abi-specific stuff
+ db, _ = NewEmptyAbiDB()
+ v = NewValidator(db)
+ )
+ testcases := []txtestcase{
+ // Invalid to checksum
+ {from: "000000000000000000000000000000000000dead", to: "000000000000000000000000000000000000dead",
+ n: "0x01", g: "0x20", gp: "0x40", value: "0x01", numMessages: 1},
+ // valid 0x000000000000000000000000000000000000dEaD
+ {from: "000000000000000000000000000000000000dead", to: "0x000000000000000000000000000000000000dEaD",
+ n: "0x01", g: "0x20", gp: "0x40", value: "0x01", numMessages: 0},
+ // conflicting input and data
+ {from: "000000000000000000000000000000000000dead", to: "0x000000000000000000000000000000000000dEaD",
+ n: "0x01", g: "0x20", gp: "0x40", value: "0x01", d: "0x01", i: "0x02", expectErr: true},
+ // Data can't be parsed
+ {from: "000000000000000000000000000000000000dead", to: "0x000000000000000000000000000000000000dEaD",
+ n: "0x01", g: "0x20", gp: "0x40", value: "0x01", d: "0x0102", numMessages: 1},
+ // Data (on Input) can't be parsed
+ {from: "000000000000000000000000000000000000dead", to: "0x000000000000000000000000000000000000dEaD",
+ n: "0x01", g: "0x20", gp: "0x40", value: "0x01", i: "0x0102", numMessages: 1},
+ // Send to 0
+ {from: "000000000000000000000000000000000000dead", to: "0x0000000000000000000000000000000000000000",
+ n: "0x01", g: "0x20", gp: "0x40", value: "0x01", numMessages: 1},
+ // Create empty contract (no value)
+ {from: "000000000000000000000000000000000000dead", to: "",
+ n: "0x01", g: "0x20", gp: "0x40", value: "0x00", numMessages: 1},
+ // Create empty contract (with value)
+ {from: "000000000000000000000000000000000000dead", to: "",
+ n: "0x01", g: "0x20", gp: "0x40", value: "0x01", expectErr: true},
+ // Small payload for create
+ {from: "000000000000000000000000000000000000dead", to: "",
+ n: "0x01", g: "0x20", gp: "0x40", value: "0x01", d: "0x01", numMessages: 1},
+ }
+ for i, test := range testcases {
+ msgs, err := v.ValidateTransaction(dummyTxArgs(test), nil)
+ if err == nil && test.expectErr {
+ t.Errorf("Test %d, expected error", i)
+ for _, msg := range msgs.Messages {
+ fmt.Printf("* %s: %s\n", msg.Typ, msg.Message)
+ }
+ }
+ if err != nil && !test.expectErr {
+ t.Errorf("Test %d, unexpected error: %v", i, err)
+ }
+ if err == nil {
+ got := len(msgs.Messages)
+ if got != test.numMessages {
+ for _, msg := range msgs.Messages {
+ fmt.Printf("* %s: %s\n", msg.Typ, msg.Message)
+ }
+ t.Errorf("Test %d, expected %d messages, got %d", i, test.numMessages, got)
+ } else {
+ //Debug printout, remove later
+ for _, msg := range msgs.Messages {
+ fmt.Printf("* [%d] %s: %s\n", i, msg.Typ, msg.Message)
+ }
+ fmt.Println()
+ }
+ }
+ }
+}