signer, rules, storage: implement rules + ephemeral storage for signer rules

This commit is contained in:
Martin Holst Swende 2018-02-05 14:44:33 +01:00
parent 3a9bfba76a
commit da312a1dcb
No known key found for this signature in database
GPG key ID: 683B438C05A5DDF0
6 changed files with 810 additions and 0 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,21 @@
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
// Package deps contains the console JavaScript dependencies Go embedded.
package deps
//go:generate go-bindata -nometadata -pkg deps -o bindata.go bignumber.js
//go:generate gofmt -w -s bindata.go

158
cmd/signer/rules/rules.go Normal file
View file

@ -0,0 +1,158 @@
// Copyright 2017 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package rules
import (
"encoding/json"
"fmt"
"github.com/ethereum/go-ethereum/cmd/signer"
"github.com/ethereum/go-ethereum/cmd/signer/rules/deps"
"github.com/ethereum/go-ethereum/cmd/signer/storage"
"github.com/ethereum/go-ethereum/log"
"github.com/robertkrimen/otto"
"os"
"strings"
)
var (
BigNumber_JS = deps.MustAsset("bignumber.js")
)
// consoleOutput is an override for the console.log and console.error methods to
// stream the output into the configured output stream instead of stdout.
func consoleOutput(call otto.FunctionCall) otto.Value {
output := []string{"JS:> "}
for _, argument := range call.ArgumentList {
output = append(output, fmt.Sprintf("%v", argument))
}
fmt.Fprintln(os.Stdout, strings.Join(output, " "))
return otto.Value{}
}
// rulesetUi provides an implementation of SignerUI that evaluates a javascript
// file for each defined UI-method
type rulesetUi struct {
vm *otto.Otto // The JS vm
next signer.SignerUI // The next handler, for manual processing
storage storage.Storage
}
func NewRuleEvaluator() (*rulesetUi, error) {
c := &rulesetUi{
vm: otto.New(),
storage: storage.NewEphemeralStorage(),
}
consoleObj, _ := c.vm.Get("console")
consoleObj.Object().Set("log", consoleOutput)
consoleObj.Object().Set("error", consoleOutput)
c.vm.Set("storage", c.storage)
return c, nil
}
func (r *rulesetUi) Init(javascriptRules string) error {
script, err := r.vm.Compile("bignumber.js", BigNumber_JS)
if err != nil {
log.Warn("Failed loading libraries", "err", err)
return err
}
r.vm.Run(script)
_, err = r.vm.Run(javascriptRules)
if err != nil {
log.Warn("Execution failed", "err", err)
}
return err
}
func (r *rulesetUi) checkApproval(jsfunc string, jsarg []byte, err error) error {
if err != nil {
return err
}
v, err := r.vm.Call("ApproveTx", nil, string(jsarg))
if err != nil {
log.Info("error occurred during execution", "error", err)
return err
}
result, err := v.ToString()
if err != nil {
log.Info("error occurred during response unmarshalling", "error", err)
return err
}
if result == "Approve" {
log.Info("Op approved")
return nil
}
return fmt.Errorf("rejected")
}
func (r *rulesetUi) ApproveTx(request *signer.SignTxRequest) (signer.SignTxResponse, error) {
jsonreq, err := json.Marshal(request)
if err = r.checkApproval("ApproveTx", jsonreq, err); err == nil {
return signer.SignTxResponse{Transaction: request.Transaction, From: request.From, Approved: true, Password: ""}, nil
}
return signer.SignTxResponse{Approved: false}, err
}
func (r *rulesetUi) ApproveSignData(request *signer.SignDataRequest) (signer.SignDataResponse, error) {
jsonreq, err := json.Marshal(request)
if err = r.checkApproval("ApproveTx", jsonreq, err); err == nil {
return signer.SignDataResponse{Approved: true, Password: ""}, nil
}
return signer.SignDataResponse{Approved: false, Password: ""}, err
}
func (r *rulesetUi) ApproveExport(request *signer.ExportRequest) (signer.ExportResponse, error) {
jsonreq, err := json.Marshal(request)
if err = r.checkApproval("ApproveTx", jsonreq, err); err == nil {
return signer.ExportResponse{Approved: true}, nil
}
return signer.ExportResponse{Approved: false}, err
}
func (r *rulesetUi) ApproveImport(request *signer.ImportRequest) (signer.ImportResponse, error) {
// This cannot be handled by rules, requires setting a password
// dispatch to next
return r.next.ApproveImport(request)
}
func (r *rulesetUi) ApproveListing(request *signer.ListRequest) (signer.ListResponse, error) {
jsonreq, err := json.Marshal(request)
if err = r.checkApproval("ApproveListing", jsonreq, err); err == nil {
return signer.ListResponse{Accounts: request.Accounts}, nil
}
return signer.ListResponse{}, err
}
func (r *rulesetUi) ApproveNewAccount(request *signer.NewAccountRequest) (signer.NewAccountResponse, error) {
// This cannot be handled by rules, requires setting a password
// dispatch to next
return r.next.ApproveNewAccount(request)
}
func (r *rulesetUi) ShowError(message string) {
log.Error(message)
r.next.ShowError(message)
}
func (r *rulesetUi) ShowInfo(message string) {
log.Info(message)
r.next.ShowInfo(message)
}

View file

@ -0,0 +1,316 @@
package rules
import (
"fmt"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/cmd/signer"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"math/big"
"testing"
)
const JS = `
/**
This is an example implementation of a Javascript rule file.
When the signer receives a request over the external API, the corresponding method is evaluated.
Three things can happen:
1. The method returns "Approve". This means the operation is permitted.
2. The method returns "Reject". This means the operation is rejected.
3. Anything else; other return values [*], method not implemented or exception occurred during processing. This means
that the operation will continue to manual processing, via the regular UI method chosen by the user.
[*] Note: Future version of the ruleset may use more complex json-based returnvalues, making it possible to not
only respond Approve/Reject/Manual, but also modify responses. For example, choose to list only one, but not all
accounts in a list-request. The points above will continue to hold for non-json based responses ("Approve"/"Reject").
**/
function ApproveListing(request){
console.log("In js approve listing");
console.log(request.accounts[3].Address)
console.log(request.meta.Remote)
return "Approve"
}
function ApproveTx(request){
console.log("test");
console.log("from");
return "Reject";
}
function test(thing){
console.log(thing.String())
}
`
func hexAddr(a string) common.Address { return common.BytesToAddress(common.Hex2Bytes(a)) }
func mixAddr(a string) (*common.MixedcaseAddress, error) {
return common.NewMixedcaseAddressFromString(a)
}
func initRuleEngine(js string) (*rulesetUi, error) {
r, err := NewRuleEvaluator()
if err != nil {
return nil, fmt.Errorf("Failed to create js engine: %v", err)
}
if err = r.Init(js); err != nil {
return nil, fmt.Errorf("Failed to load bootstrap js: %v", err)
}
return r, nil
}
func TestListRequest(t *testing.T) {
accs := make([]signer.Account, 5)
for i, _ := range accs {
addr := fmt.Sprintf("000000000000000000000000000000000000000%x", i)
acc := signer.Account{
Address: common.BytesToAddress(common.Hex2Bytes(addr)),
URL: accounts.URL{Scheme: "test", Path: fmt.Sprintf("acc-%d", i)},
}
accs[i] = acc
}
js := `function ApproveListing(accounts, meta){ return "Approve" }`
r, err := initRuleEngine(js)
if err != nil {
t.Errorf("Couldn't create evaluator %v", err)
return
}
resp, err := r.ApproveListing(&signer.ListRequest{
accs,
signer.Metadata{
"remoteip", "localip", "inproc",
},
})
if len(resp.Accounts) != len(accs) {
t.Errorf("Expected check to resolve to 'Approve'")
}
}
func TestSignTxRequest(t *testing.T) {
js := `
function ApproveTx(jsonstr){
console.log(jsonstr)
r = JSON.parse(jsonstr)
console.log("from", r.from)
console.log("transaction.to", r.transaction.to);
console.log("transaction.value", r.transaction.value);
console.log("transaction.nonce", r.transaction.nonce);
if(r.from.toLowerCase()=="0x0000000000000000000000000000000000001337"){ return "Approve"}
if(r.from.toLowerCase()=="0x000000000000000000000000000000000000dead"){ return "Reject"}
}`
r, err := initRuleEngine(js)
if err != nil {
t.Errorf("Couldn't create evaluator %v", err)
return
}
to, err := mixAddr("000000000000000000000000000000000000dead")
if err != nil {
t.Error(err)
return
}
from, err := mixAddr("0000000000000000000000000000000000001337")
if err != nil {
t.Error(err)
return
}
fmt.Printf("to %v", to.Address().String())
resp, err := r.ApproveTx(&signer.SignTxRequest{
Transaction: signer.TransactionArg{To: to},
From: *from,
Callinfo: "",
Meta: signer.Metadata{"remoteip", "localip", "inproc"},
})
if err != nil {
t.Errorf("Unexpected error %v", err)
}
if !resp.Approved {
t.Errorf("Expected check to resolve to 'Approve'")
}
}
func TestMissingFunc(t *testing.T) {
r, err := initRuleEngine(JS)
if err != nil {
t.Errorf("Couldn't create evaluator %v", err)
return
}
_, err = r.vm.Call("MissingMethod", nil, "test")
if err == nil {
t.Error("Expected error")
}
if r.checkApproval("MissingMethod", nil, nil) == nil {
t.Errorf("Expected error to resolve to 'Reject'")
}
fmt.Printf("Err %v", err)
}
func TestStorage(t *testing.T) {
js := `
function testStorage(){
storage.Put("mykey", "myvalue")
a = storage.Get("mykey")
storage.Put("mykey", ["a", "list"]) // Should result in "a,list"
a += storage.Get("mykey")
storage.Put("mykey", {"an": "object"}) // Should result in "[object Object]"
a += storage.Get("mykey")
storage.Put("mykey", JSON.stringify({"an": "object"})) // Should result in '{"an":"object"}'
a += storage.Get("mykey")
a += storage.Get("missingkey") //Missing keys should result in empty string
storage.Put("","missing key==noop") // Can't store with 0-length key
a += storage.Get("") // Should result in ''
var b = new BigNumber(2)
var c = new BigNumber(16)//"0xf0",16)
var d = b.plus(c)
console.log(d)
return a
}
`
r, err := initRuleEngine(js)
if err != nil {
t.Errorf("Couldn't create evaluator %v", err)
return
}
v, err := r.vm.Call("testStorage", nil, nil)
if err != nil {
t.Errorf("Unexpected error %v", err)
}
retval, err := v.ToString()
if err != nil {
t.Errorf("Unexpected error %v", err)
}
exp := `myvaluea,list[object Object]{"an":"object"}`
if retval != exp {
t.Errorf("Unexpected data, expected '%v', got '%v'", exp, retval)
}
fmt.Printf("Err %v", err)
}
const ExampleTxWindow = `
function big(str){
if(str.slice(0,2) == "0x"){ return new BigNumber(str.slice(2),16)}
return new BigNumber(str)
}
// Time window: 1 week
var window = 1000* 3600*24*7;
// Limit : 1 ether
var limit = new BigNumber("1e18");
function isLimitOk(transaction){
var value = big(transaction.value)
// Start of our window function
var windowstart = new Date().getTime() - window;
var txs = [];
var stored = storage.Get('txs');
if(stored != ""){
txs = JSON.parse(stored)
}
// First, remove all that have passed out of the time-window
var newtxs = txs.filter(function(tx){return tx.tstamp > windowstart});
console.log(txs, newtxs.length);
// Secondly, aggregate the current sum
sum = new BigNumber(0)
sum = newtxs.reduce(function(agg, tx){ return big(tx.value).plus(agg)}, sum);
console.log("Sum so far", sum);
console.log("Requested", value.toNumber());
// Would we exceed weekly limit ?
if (sum.plus(value).lt(limit)){
// Add this to the storage
newtxs.push({tstamp: new Date().getTime(), value: value});
storage.Put("txs", JSON.stringify(newtxs));
return true;
}
return false;
}
function ApproveTx(jsonstr){
r = JSON.parse(jsonstr);
console.log("Requested value ", r.transaction.value)
if (isLimitOk(r.transaction)){
return "Approve"
}
return "Nope"
}
`
func dummyTx(value *hexutil.Big) *signer.SignTxRequest {
to, _ := mixAddr("000000000000000000000000000000000000dead")
from, _ := mixAddr("000000000000000000000000000000000000dead")
return &signer.SignTxRequest{
Transaction: signer.TransactionArg{
To: to,
Value: value,
},
From: *from,
Callinfo: "Warning, all your base are bellong to us",
Meta: signer.Metadata{"remoteip", "localip", "inproc"},
}
}
func TestLimitWindow(t *testing.T) {
r, err := initRuleEngine(ExampleTxWindow)
if err != nil {
t.Errorf("Couldn't create evaluator %v", err)
return
}
if err != nil {
t.Error(err)
return
}
// 0.3 ether: 429D069189E0000 wei
v := big.NewInt(0).SetBytes(common.Hex2Bytes("0429D069189E0000"))
h := hexutil.Big(*v)
// The first three should succeed
for i := 0; i < 3; i++ {
resp, err := r.ApproveTx(dummyTx(&h))
if err != nil {
t.Errorf("Unexpected error %v", err)
}
if !resp.Approved {
t.Errorf("Expected check to resolve to 'Approve'")
}
}
// Fourth should fail
resp, err := r.ApproveTx(dummyTx(&h))
if resp.Approved {
t.Errorf("Expected check to resolve to 'Reject'")
}
}

View file

@ -0,0 +1,76 @@
// 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 storage
import (
"fmt"
)
type Storage interface {
// Put stores a value by key. 0-length keys results in no-op
Put(key, value string)
// Get returns the previously stored value, or the empty string if it does not exist or key is of 0-length
Get(key string) string
// New creates a new (sub) namespace for the storage
New(namespace string) Storage
}
// EphemeralStorage is an in-memory storage that does
// not persist values to disk. Mainly used for testing
type EphemeralStorage struct {
data map[string]string
namespace string
}
func (s *EphemeralStorage) Put(key, value string) {
if len(key) == 0 {
return
}
key = fmt.Sprintf("%s.%s", s.namespace, key)
fmt.Printf("storage: put %v -> %v\n", key, value)
s.data[key] = value
}
func (s *EphemeralStorage) Get(key string) string {
if len(key) == 0 {
return ""
}
key = fmt.Sprintf("%s.%s", s.namespace, key)
fmt.Printf("storage: get %v\n", key)
if v, exist := s.data[key]; exist {
return v
}
return ""
}
func (s *EphemeralStorage) New(namespace string) Storage {
child := &EphemeralStorage{
data: make(map[string]string),
namespace: fmt.Sprintf("%s.%s", namespace),
}
return child
}
func NewEphemeralStorage() Storage {
s := &EphemeralStorage{
data: make(map[string]string),
namespace: "root",
}
return s
}