go-ethereum/signer/core/api_test.go
Paul Berg 572baae10a signer, clef: implement EIP191/712 (#17789)
* Named functions and defined a basic EIP191 content type list

* Written basic content type functions

* Added ecRecover method in the clef api

* Updated the extapi changelog and addded indications in the README

* Changed the version of the external API

* Added tests for 0x45

* Implementing UnmarshalJSON() for TypedData

* Working on TypedData

* Solved the auditlog issue

* Changed method to signTypedData

* Changed mimes and implemented the 'encodeType' function for EIP-712

* Polished docstrings, ran goimports and swapped fmt.Errorf with errors.New where possible

* Drafted recursive encodeData

* Ran goimports and gofmt

* Drafted first version of EIP-712, including tests

* Temporarily switched to using common.Address in tests

* Drafted text/validator and and rewritten []byte as hexutil.Bytes

* Solved stringified address encoding issue

* Changed the property type required by signData from bytes to interface{}

* Fixed bugs in 'data/typed' signs

* Brought legal warning back after temporarily disabling it for development

* Added example RPC calls for account_signData and account_signTypedData

* Named functions and defined a basic EIP191 content type list

* Written basic content type functions

* Added ecRecover method in the clef api

* Updated the extapi changelog and addded indications in the README

* Added tests for 0x45

* Implementing UnmarshalJSON() for TypedData

* Working on TypedData

* Solved the auditlog issue

* Changed method to signTypedData

* Changed mimes and implemented the 'encodeType' function for EIP-712

* Polished docstrings, ran goimports and swapped fmt.Errorf with errors.New where possible

* Drafted recursive encodeData

* Ran goimports and gofmt

* Drafted first version of EIP-712, including tests

* Temporarily switched to using common.Address in tests

* Drafted text/validator and and rewritten []byte as hexutil.Bytes

* Solved stringified address encoding issue

* Changed the property type required by signData from bytes to interface{}

* Fixed bugs in 'data/typed' signs

* Brought legal warning back after temporarily disabling it for development

* Added example RPC calls for account_signData and account_signTypedData

* Polished and fixed PR

* Polished and fixed PR

* Solved malformed data panics and also wrote tests

* Solved malformed data panics and also wrote tests

* Added alphabetical sorting to type dependencies

* Added alphabetical sorting to type dependencies

* Added pretty print to data/typed UI

* Added pretty print to data/typed UI

* signer: more tests for typed data

* signer: more tests for typed data

* Fixed TestMalformedData4 errors and renamed IsValid to Validate

* Fixed TestMalformedData4 errors and renamed IsValid to Validate

* Fixed more new failing tests and deanonymised some functions

* Fixed more new failing tests and deanonymised some functions

* Added types to EIP712 output in cliui

* Added types to EIP712 output in cliui

* Fixed regexp issues

* Fixed regexp issues

* Added pseudo-failing test

* Added pseudo-failing test

* Fixed false positive test

* Fixed false positive test

* Added PrettyPrint method

* Added PrettyPrint method

* signer: refactor formatting and UI

* signer: make ui use new message format for signing

* Fixed breaking changes

* Fixed rules_test failing test

* Added extra regexp for reference types

* signer: more hard types

* Fixed failing test, formatted files

* signer: use golang/x keccak

* Fixed goimports error

* clef, signer: address some review concerns

* Implemented latest recommendations

* Fixed comments and uintint256 issue

* accounts, signer: fix mimetypes, add interface to sign data with passphrase

* signer, accounts: remove duplicated code, pass hash preimages to signing

* signer: prevent panic in type assertions, make cliui print rawdata as quotable-safe

* signer: linter fixes, remove deprecated crypto dependency

* accounts: fix goimport
2019-02-06 08:30:49 +01:00

374 lines
9.3 KiB
Go

// 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"
"errors"
"fmt"
"io/ioutil"
"math/big"
"os"
"path/filepath"
"testing"
"time"
"github.com/ethereum/go-ethereum/accounts/keystore"
"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) OnInputRequired(info UserInputRequest) (UserInputResponse, error) {
return UserInputResponse{}, errors.New("not implemented")
}
func (ui *HeadlessUI) OnSignerStartup(info StartupInfo) {
}
func (ui *HeadlessUI) OnApprovedTx(tx ethapi.SignTransactionResult) {
fmt.Printf("OnApproved()\n")
}
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.Fprintln(os.Stderr, message)
}
func (ui *HeadlessUI) ShowInfo(message string) {
//stdout is used by communication
fmt.Fprintln(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, 20)
db, err := NewAbiDBFromFile("../../cmd/clef/4byte.json")
if err != nil {
t.Fatal(err.Error())
}
var (
ui = &HeadlessUI{controller}
api = NewSignerAPI(
1,
tmpDirName(t),
true,
ui,
db,
true, true)
)
return api, controller
}
func createAccount(control chan string, api *SignerAPI, t *testing.T) {
control <- "Y"
control <- "a_long_password"
_, err := api.New(context.Background())
if err != nil {
t.Fatal(err)
}
// Some time to allow changes to propagate
time.Sleep(250 * time.Millisecond)
}
func failCreateAccountWithPassword(control chan string, api *SignerAPI, password string, t *testing.T) {
control <- "Y"
control <- password
control <- "Y"
control <- password
control <- "Y"
control <- password
acc, err := api.New(context.Background())
if err == nil {
t.Fatal("Should have returned an error")
}
if acc.Address != (common.Address{}) {
t.Fatal("Empty address should be returned")
}
}
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) []common.Address {
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)
// Fail to create this, due to bad password
failCreateAccountWithPassword(control, api, "short", t)
failCreateAccountWithPassword(control, api, "longerbutbad\rfoo", 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 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 []common.Address
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])
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 <- "a_long_password"
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 <- "a_long_password"
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 <- "a_long_password"
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 <- "a_long_password"
control <- "Y" //
control <- "a_long_password"
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)
}
*/