diff --git a/cmd/clef/audit.log b/cmd/clef/audit.log index a38552de7f..652dc04ae8 100644 --- a/cmd/clef/audit.log +++ b/cmd/clef/audit.log @@ -203,3 +203,22 @@ t=2018-10-21T20:03:51+0100 lvl=info msg=Configured api=signer audit log=audit.lo t=2018-10-21T20:03:53+0100 lvl=info msg=SignTypedData api=signer type=request metadata="{\"remote\":\"127.0.0.1:55028\",\"local\":\"localhost:8550\",\"scheme\":\"HTTP/1.1\",\"User-Agent\":\"PostmanRuntime/7.3.0\",\"Origin\":\"\"}" addr="0x66D76e6A80DC7D46d7EC1b79b15Dfa34c3C6eF21 [chksum ok]" data="{Types:map[EIP712Domain:[map[type:string name:name] map[name:version type:string] map[name:chainId type:uint256] map[name:verifyingContract type:address]] Person:[map[name:name type:string] map[name:wallet type:address]] Mail:[map[type:Person name:from] map[name:to type:Person] map[name:contents type:string]]] PrimaryType:Mail Domain:{Name:Ether Mail Version:1 ChainId:+1 VerifyingContract:[204 204 204 204 204 204 204 204 204 204 204 204 204 204 204 204 204 204 204 204] Salt:0x} Message:map[contents:Hello, Bob! from:map[name:Alice wallet:0x66d76e6a80dc7d46d7ec1b79b15dfa34c3c6ef21] to:map[name:Bob wallet:0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB]]}" t=2018-10-21T20:17:01+0100 lvl=info msg=Configured api=signer audit log=audit.log t=2018-10-21T20:17:44+0100 lvl=info msg=SignTypedData api=signer type=request metadata="{\"remote\":\"127.0.0.1:55221\",\"local\":\"localhost:8550\",\"scheme\":\"HTTP/1.1\",\"User-Agent\":\"PostmanRuntime/7.3.0\",\"Origin\":\"\"}" addr="0x66D76e6A80DC7D46d7EC1b79b15Dfa34c3C6eF21 [chksum ok]" data="{Types:map[EIP712Domain:[map[name:name type:string] map[name:version type:string] map[name:chainId type:uint256] map[name:verifyingContract type:address]] Person:[map[name:name type:string] map[type:address name:wallet]] Mail:[map[name:from type:Person] map[name:to type:Person] map[name:contents type:string]]] PrimaryType:Mail Domain:{Name:Ether Mail Version:1 ChainId:+1 VerifyingContract:[204 204 204 204 204 204 204 204 204 204 204 204 204 204 204 204 204 204 204 204] Salt:0x} Message:map[to:map[name:Bob wallet:0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB] contents:Hello, Bob! from:map[name:Alice wallet:0x66d76e6a80dc7d46d7ec1b79b15dfa34c3c6ef21]]}" +t=2018-10-23T18:55:35+0100 lvl=info msg=Configured api=signer audit log=audit.log +t=2018-10-23T19:36:14+0100 lvl=info msg=Configured api=signer audit log=audit.log +t=2018-10-23T19:38:40+0100 lvl=info msg=SignTypedData api=signer type=request metadata="{\"remote\":\"127.0.0.1:55416\",\"local\":\"localhost:8550\",\"scheme\":\"HTTP/1.1\",\"User-Agent\":\"PostmanRuntime/7.3.0\",\"Origin\":\"\"}" addr="0x66D76e6A80DC7D46d7EC1b79b15Dfa34c3C6eF21 [chksum ok]" data="{Types:map[Person:[map[name:name type:string] map[type:address name:wallet]] Mail:[map[name:from type:Person] map[name:to type:Person] map[name:contents type:string]] EIP712Domain:[map[name:name type:string] map[name:version type:string] map[name:chainId type:uint256] map[name:verifyingContract type:address]]] PrimaryType:Mail Domain:{Name:Ether Mail Version:1 ChainId:+1 VerifyingContract:[204 204 204 204 204 204 204 204 204 204 204 204 204 204 204 204 204 204 204 204] Salt:0x} Message:map[from:map[name:Alice wallet:0x66d76e6a80dc7d46d7ec1b79b15dfa34c3c6ef21] to:map[name:Bob wallet:0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB] contents:Hello, Bob!]}" +t=2018-10-23T19:46:18+0100 lvl=info msg=Configured api=signer audit log=audit.log +t=2018-10-23T19:46:45+0100 lvl=info msg=Configured api=signer audit log=audit.log +t=2018-10-23T19:46:48+0100 lvl=info msg=SignTypedData api=signer type=request metadata="{\"remote\":\"127.0.0.1:55481\",\"local\":\"localhost:8550\",\"scheme\":\"HTTP/1.1\",\"User-Agent\":\"PostmanRuntime/7.3.0\",\"Origin\":\"\"}" addr="0x66D76e6A80DC7D46d7EC1b79b15Dfa34c3C6eF21 [chksum ok]" data="{Types:map[EIP712Domain:[map[name:name type:string] map[name:version type:string] map[name:chainId type:uint256] map[name:verifyingContract type:address]] Person:[map[name:name type:string] map[name:wallet type:address]] Mail:[map[name:from type:Person] map[name:to type:Person] map[name:contents type:string]]] PrimaryType:Mail Domain:{Name:Ether Mail Version:1 ChainId:+1 VerifyingContract:0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC Salt:0x} Message:map[from:map[wallet:0x66d76e6a80dc7d46d7ec1b79b15dfa34c3c6ef21 name:Alice] to:map[name:Bob wallet:0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB] contents:Hello, Bob!]}" +t=2018-10-23T19:47:53+0100 lvl=info msg=Configured api=signer audit log=audit.log +t=2018-10-23T19:48:10+0100 lvl=info msg=Configured api=signer audit log=audit.log +t=2018-10-23T19:48:14+0100 lvl=info msg=SignTypedData api=signer type=request metadata="{\"remote\":\"127.0.0.1:55506\",\"local\":\"localhost:8550\",\"scheme\":\"HTTP/1.1\",\"User-Agent\":\"PostmanRuntime/7.3.0\",\"Origin\":\"\"}" addr="0x66D76e6A80DC7D46d7EC1b79b15Dfa34c3C6eF21 [chksum ok]" data="{Types:map[EIP712Domain:[map[name:name type:string] map[name:version type:string] map[name:chainId type:uint256] map[name:verifyingContract type:address]] Person:[map[name:name type:string] map[name:wallet type:address]] Mail:[map[name:from type:Person] map[name:to type:Person] map[name:contents type:string]]] PrimaryType:Mail Domain:{Name:Ether Mail Version:1 ChainId:+1 VerifyingContract:0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC Salt:0x} Message:map[to:map[name:Bob wallet:0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB] contents:Hello, Bob! from:map[name:Alice wallet:0x66d76e6a80dc7d46d7ec1b79b15dfa34c3c6ef21]]}" +t=2018-10-23T19:48:57+0100 lvl=info msg=Configured api=signer audit log=audit.log +t=2018-10-23T19:49:10+0100 lvl=info msg=Configured api=signer audit log=audit.log +t=2018-10-23T19:49:16+0100 lvl=info msg=SignTypedData api=signer type=request metadata="{\"remote\":\"127.0.0.1:55529\",\"local\":\"localhost:8550\",\"scheme\":\"HTTP/1.1\",\"User-Agent\":\"PostmanRuntime/7.3.0\",\"Origin\":\"\"}" addr="0x66D76e6A80DC7D46d7EC1b79b15Dfa34c3C6eF21 [chksum ok]" data="{Types:map[EIP712Domain:[map[name:name type:string] map[name:version type:string] map[name:chainId type:uint256] map[name:verifyingContract type:address]] Person:[map[name:name type:string] map[name:wallet type:address]] Mail:[map[name:from type:Person] map[type:Person name:to] map[name:contents type:string]]] PrimaryType:Mail Domain:{Name:Ether Mail Version:1 ChainId:+1 VerifyingContract:0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC Salt:0x} Message:map[contents:Hello, Bob! from:map[wallet:0x66d76e6a80dc7d46d7ec1b79b15dfa34c3c6ef21 name:Alice] to:map[name:Bob wallet:0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB]]}" +t=2018-10-23T19:49:49+0100 lvl=info msg=Configured api=signer audit log=audit.log +t=2018-10-23T19:49:57+0100 lvl=info msg=SignTypedData api=signer type=request metadata="{\"remote\":\"127.0.0.1:55542\",\"local\":\"localhost:8550\",\"scheme\":\"HTTP/1.1\",\"User-Agent\":\"PostmanRuntime/7.3.0\",\"Origin\":\"\"}" addr="0x66D76e6A80DC7D46d7EC1b79b15Dfa34c3C6eF21 [chksum ok]" data="{Types:map[EIP712Domain:[map[name:name type:string] map[name:version type:string] map[name:chainId type:uint256] map[name:verifyingContract type:address]] Person:[map[name:name type:string] map[name:wallet type:address]] Mail:[map[name:from type:Person] map[name:to type:Person] map[name:contents type:string]]] PrimaryType:Mail Domain:{Name:Ether Mail Version:1 ChainId:+1 VerifyingContract:0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC Salt:0x} Message:map[from:map[name:Alice wallet:0x66d76e6a80dc7d46d7ec1b79b15dfa34c3c6ef21] to:map[name:Bob wallet:0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB] contents:Hello, Bob!]}" +t=2018-10-23T19:51:44+0100 lvl=info msg=Configured api=signer audit log=audit.log +t=2018-10-23T19:51:57+0100 lvl=info msg=Configured api=signer audit log=audit.log +t=2018-10-23T19:52:05+0100 lvl=info msg=SignTypedData api=signer type=request metadata="{\"remote\":\"127.0.0.1:55568\",\"local\":\"localhost:8550\",\"scheme\":\"HTTP/1.1\",\"User-Agent\":\"PostmanRuntime/7.3.0\",\"Origin\":\"\"}" addr="0x66D76e6A80DC7D46d7EC1b79b15Dfa34c3C6eF21 [chksum ok]" data="{Types:map[Person:[map[name:name type:string] map[name:wallet type:address]] Mail:[map[name:from type:Person] map[name:to type:Person] map[name:contents type:string]] EIP712Domain:[map[type:string name:name] map[name:version type:string] map[name:chainId type:uint256] map[name:verifyingContract type:address]]] PrimaryType:Mail Domain:{Name:Ether Mail Version:1 ChainId:+1 VerifyingContract:0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC Salt:0x} Message:map[contents:Hello, Bob! from:map[name:Alice wallet:0x66d76e6a80dc7d46d7ec1b79b15dfa34c3c6ef21] to:map[name:Bob wallet:0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB]]}" +t=2018-10-23T20:12:04+0100 lvl=info msg=Configured api=signer audit log=audit.log +t=2018-10-23T20:12:14+0100 lvl=info msg=SignTypedData api=signer type=request metadata="{\"remote\":\"127.0.0.1:55752\",\"local\":\"localhost:8550\",\"scheme\":\"HTTP/1.1\",\"User-Agent\":\"PostmanRuntime/7.3.0\",\"Origin\":\"\"}" addr="0x66D76e6A80DC7D46d7EC1b79b15Dfa34c3C6eF21 [chksum ok]" data="{Types:map[EIP712Domain:[map[name:name type:string] map[name:version type:string] map[name:chainId type:uint256] map[name:verifyingContract type:address]] Person:[map[name:name type:string] map[name:wallet type:address]] Mail:[map[name:from type:Person] map[name:to type:Person] map[type:string name:contents]]] PrimaryType:Mail Domain:{Name:Ether Mail Version:1 ChainId:+1 VerifyingContract:0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC Salt:0x} Message:map[from:map[name:Alice wallet:0x66d76e6a80dc7d46d7ec1b79b15dfa34c3c6ef21] to:map[name:Bob wallet:0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB] contents:Hello, Bob!]}" diff --git a/cmd/clef/main.go b/cmd/clef/main.go index 648dd59306..3c872e628d 100644 --- a/cmd/clef/main.go +++ b/cmd/clef/main.go @@ -332,7 +332,7 @@ func initialize(c *cli.Context) error { // If using the stdioui, we can't do the 'confirm'-flow fmt.Fprintf(logOutput, legalWarning) } else { - // Temporarily disabled while in development + // Temporarily disabled //if !confirm(legalWarning) { // return fmt.Errorf("aborted by user") //} @@ -558,7 +558,7 @@ func readMasterKey(ctx *cli.Context, ui core.SignerUI) ([]byte, error) { var password string // If ui is not nil, get the password from ui. if ui != nil { - // Temporarily disabled while in development + // Temporarily disabled //resp, err := ui.OnInputRequired(core.UserInputRequest{ // Title: "Master Password", // Prompt: "Please enter the password to decrypt the master seed", diff --git a/signer/core/api_test.go b/signer/core/api_test.go index f1695feec9..ff68c0b4e1 100644 --- a/signer/core/api_test.go +++ b/signer/core/api_test.go @@ -245,72 +245,6 @@ func TestNewAcc(t *testing.T) { } } -func signTextValidator(t *testing.T) { - // TODO -} - -func signApplicationClique(t *testing.T) { - // TODO -} - -func signTextPlain(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]) - - control <- "Y" - control <- "wrongpassword" - h, err := api.SignData(context.Background(), TextPlain.Mime, 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.SignData(context.Background(), TextPlain.Mime, 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 <- "a_long_password" - h, err = api.SignData(context.Background(), TextPlain.Mime, 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 signTypedData(t *testing.T) { - // TODO -} - -func TestSignData(t *testing.T) { - // application/validator or `0x00` - signTextValidator(t) - - // data/structured `0x01` - signTypedData(t) - - // application/clique or `0x02` - signApplicationClique(t) - - // text/plain or `0x45` - signTextPlain(t) -} - func mkTestTx(from common.MixedcaseAddress) SendTxArgs { to := common.NewMixedcaseAddress(common.HexToAddress("0x1337")) gas := hexutil.Uint64(21000) diff --git a/signer/core/signed_data.go b/signer/core/signed_data.go index bf12a3ba45..2b74cb62ec 100644 --- a/signer/core/signed_data.go +++ b/signer/core/signed_data.go @@ -1,17 +1,31 @@ +// 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 . +// package core import ( "bytes" "context" - "encoding/hex" "encoding/json" "errors" "fmt" - "github.com/ethereum/go-ethereum/common/math" "math/big" "mime" "reflect" - "sort" + "strconv" "strings" "unicode" @@ -44,11 +58,11 @@ type EIP712TypePriority struct { type EIP712Data = map[string]interface{} type EIP712Domain struct { - Name string `json:"name"` - Version string `json:"version"` - ChainId *big.Int `json:"chainId"` - VerifyingContract common.Address `json:"verifyingContract"` - Salt hexutil.Bytes `json:"salt"` + Name string `json:"name"` + Version string `json:"version"` + ChainId *big.Int `json:"chainId"` + VerifyingContract string `json:"verifyingContract"` + Salt hexutil.Bytes `json:"salt"` } const ( @@ -208,43 +222,26 @@ func signCliqueHeader(header *types.Header) (hexutil.Bytes, error) { return hash.Bytes(), nil } -// SignTypedData signs EIP712 conformant typed data +// SignTypedData signs EIP-712 conformant typed data // hash = keccak256("\x19${byteVersion}${domainSeparator}${hashStruct(message)}") func (api *SignerAPI) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, typedData TypedData) (hexutil.Bytes, error) { - domainTypes := EIP712Types{ - "EIP712Domain": typedData.Types["EIP712Domain"], - } - domainSeparatorBytes := typedData.hashStruct(domainTypes, typedData.Domain.Map(), "EIP712Domain", 0) - domainSeparator := common.BytesToHash(domainSeparatorBytes) - - domainlessTypes := make(EIP712Types) - for typeKey, typeVal := range typedData.Types { - if typeKey == "EIP712Domain" { - continue - } - domainlessTypes[typeKey] = typeVal - } - typedDataHashBytes := typedData.hashStruct(domainlessTypes, typedData.Message, typedData.PrimaryType, 0) - typedDataHash := common.BytesToHash(typedDataHashBytes) - + domainSeparator := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) + typedDataHash := typedData.HashStruct(typedData.PrimaryType, typedData.Message) typedDataJson, err := json.Marshal(typedData.Map()) if err != nil { return nil, err } - printJson("SignTypedData", typedData.Map()) - fmt.Printf("domainSeparator: %s\n", domainSeparator.String()) - fmt.Printf("typedDataHash: %s\n\n", typedDataHash.String()) - buffer := bytes.Buffer{} buffer.WriteString("\x19") - buffer.WriteString(fmt.Sprintf("\x19\\x%x", DataTyped.ByteVersion)) - buffer.Write(domainSeparator.Bytes()) - buffer.Write(typedDataHash.Bytes()) - - msg := buffer.String() - sighash := crypto.Keccak256(buffer.Bytes()) - req := &SignDataRequest{Rawdata: typedDataJson, Message: msg, Hash: sighash, ContentType: DataTyped.Mime} - + buffer.WriteString("\x01") + buffer.WriteString(common.Bytes2Hex(domainSeparator)) + buffer.WriteString(common.Bytes2Hex(typedDataHash)) + req := &SignDataRequest{ + Rawdata: typedDataJson, + Message: buffer.String(), + Hash: crypto.Keccak256(buffer.Bytes()), + ContentType: DataTyped.Mime, + } signature, err := api.Sign(ctx, addr, req) if err != nil { api.UI.ShowError(err.Error()) @@ -255,199 +252,194 @@ func (api *SignerAPI) SignTypedData(ctx context.Context, addr common.MixedcaseAd // hashStruct generates the following encoding for the given domain and message: // `encode(domainSeparator : 𝔹²⁵⁶, message : 𝕊) = "\x19\x01" ‖ domainSeparator ‖ hashStruct(message)` -func (typedData *TypedData) hashStruct(_types EIP712Types, data EIP712Data, dataType string, depth int) []byte { - typeEncoding := typedData.encodeType(_types) - typeHash := hex.EncodeToString(crypto.Keccak256(typeEncoding)) - - dataEncoding := typedData.encodeData(_types, data, dataType, depth) - dataHash := hex.EncodeToString(crypto.Keccak256(dataEncoding)) - - var buffer bytes.Buffer - buffer.WriteString(typeHash) - buffer.WriteString(dataHash) - encoding := crypto.Keccak256(buffer.Bytes()) - - if depth == 0 { - fmt.Printf("typeEncoding %s\n", common.Bytes2Hex(typeEncoding)) - fmt.Printf("dataEncoding %s\n", common.Bytes2Hex(dataEncoding)) - } - - printJson("hashStruct", map[string]interface{}{ - "depth": depth, - "encoding": buffer.String(), - }) - return encoding +func (typedData *TypedData) HashStruct(primaryType string, data EIP712Data) []byte { + return crypto.Keccak256(typedData.EncodeData(primaryType, data)) } -// encodeType generates the followign encoding: -// `name ‖ "(" ‖ member₁ ‖ "," ‖ member₂ ‖ "," ‖ … ‖ memberₙ ")"` -// -// each member is written as `type ‖ " " ‖ name` encodings cascade down and are sorted by name -func (typedData *TypedData) encodeType(_types EIP712Types) []byte { - var priorities = make(map[string]uint) - for key := range _types { - priorities[key] = 0 - } - - // Updates the priority for every new custom type discovered - update := func(typeKey string, typeVal string) { - priorities[typeVal]++ - - // Importantly, we also have to check for parent types to increment them too - for _, typeObj := range _types[typeVal] { - _typeVal := typeObj["type"] - - firstChar := []rune(_typeVal)[0] - if unicode.IsUpper(firstChar) { - priorities[_typeVal]++ - } - } - } - - // Checks if referenced type has already been visited to optimise algo - visited := func(arr []string, val string) bool { +// dependencies returns an array of custom types ordered by their +// hierarchical reference tree +func (typedData *TypedData) Dependencies(primaryType string, found []string) []string { + includes := func(arr []string, str string) bool { for _, obj := range arr { - if obj == val { + if obj == str { return true } } return false } - for typeKey, typeArr := range _types { - var typeValArr []string - for _, typeObj := range typeArr { - typeVal := typeObj["type"] - - // filtering the structs from the primitives - if _types[typeVal] != nil && !visited(typeValArr, typeVal) { - typeValArr = append(typeValArr, typeVal) - update(typeKey, typeVal) + if includes(found, primaryType) { + return found + } + if typedData.Types[primaryType] == nil { + return found + } + found = append(found, primaryType) + for _, field := range typedData.Types[primaryType] { + for _, dep := range typedData.Dependencies(field["type"], found) { + if !includes(found, dep) { + found = append(found, dep) } } - typeValArr = []string{} } + return found +} - if _types[typedData.PrimaryType] != nil { - priorities[typedData.PrimaryType] = math.MaxInt32 +// encodeType generates the following encoding: +// `name ‖ "(" ‖ member₁ ‖ "," ‖ member₂ ‖ "," ‖ … ‖ memberₙ ")"` +// +// each member is written as `type ‖ " " ‖ name` encodings cascade down and are sorted by name +func (typedData *TypedData) EncodeType(primaryType string) []byte { + // Get dependencies primary first, then alphabetical + deps := typedData.Dependencies(primaryType, []string{}) + for i, dep := range deps { + if dep == primaryType { + deps = append(deps[:i], deps[i+1:]...) + break + } } + deps = append([]string{primaryType}, deps...) - sortedPriorities := sortByPriorityAndName(priorities) + // Format as a string with fields var buffer bytes.Buffer - for _, priority := range sortedPriorities { - typeKey := priority.Type - typeArr := _types[typeKey] - - buffer.WriteString(typeKey) + for _, dep := range deps { + buffer.WriteString(dep) buffer.WriteString("(") - - for _, typeObj := range typeArr { - buffer.WriteString(typeObj["type"]) + for _, obj := range typedData.Types[dep] { + buffer.WriteString(obj["type"]) buffer.WriteString(" ") - buffer.WriteString(typeObj["name"]) + buffer.WriteString(obj["name"]) buffer.WriteString(",") } - buffer.Truncate(buffer.Len() - 1) buffer.WriteString(")") } - - printJson("encodeType", map[string]interface{}{ - "types": _types, - "encoding": buffer.String(), - }) return buffer.Bytes() } +func (typedData *TypedData) TypeHash(primaryType string) []byte { + return crypto.Keccak256(typedData.EncodeType(primaryType)) +} + +func bytesValueOf(_interface interface{}) []byte { + bytesVal, ok := _interface.([]byte) + if ok { + return bytesVal + } + + switch reflect.TypeOf(_interface) { + case reflect.TypeOf(string("")): + return []byte(_interface.(string)) + break + default: + break + } + + panic(fmt.Errorf("unrecognized interface %v", _interface)) + return []byte{} +} + // encodeData generates the following encoding: // `enc(value₁) ‖ enc(value₂) ‖ … ‖ enc(valueₙ)` // // each encoded member is 32-byte long -func (typedData *TypedData) encodeData(_types EIP712Types, data interface{}, dataType string, depth int) []byte { - var buffer bytes.Buffer +func (typedData *TypedData) EncodeData(primaryType string, data map[string]interface{}) []byte { + encTypes := []string{} + encValues := []interface{}{} - // TODO regex - // handle arrays - if strings.Contains(dataType, "[]") { - arrayVal := data.([]interface{}) - dataType := "TODO" + // Add typehash + encTypes = append(encTypes, "bytes32") + encValues = append(encValues, typedData.TypeHash(primaryType)) - var arrayBuffer bytes.Buffer - for obj := range arrayVal { - objEncoding := typedData.encodeData(_types, obj, dataType, depth+1) - arrayBuffer.Write(objEncoding) - } + // Handle primitive values + handlePrimitiveValue := func(encType string, encValue interface{}) (string, interface{}) { + var primitiveEncType string + var primitiveEncValue interface{} - encoding := arrayBuffer.Bytes() - buffer.Write(encoding) - return buffer.Bytes() - } - - // handle maps - firstChar := []rune(dataType)[0] - if unicode.IsUpper(firstChar) { - for mapKey, mapVal := range data.(EIP712Data) { - nextDataType := findNextDataType(_types, dataType, mapKey) - if reflect.TypeOf(mapVal) == reflect.TypeOf(EIP712Data{}) { - data := mapVal.(map[string]interface{}) - encoding := typedData.hashStruct(_types, data, nextDataType, depth+1) - buffer.Write(encoding) - } else { - encoding := typedData.encodeData(_types, mapVal, nextDataType, depth+1) - buffer.Write(encoding) + switch encType { + case "address": + primitiveEncType = "address" + bytesValue := []byte{} + for i := 0; i < 12; i++ { + bytesValue = append(bytesValue, 0) } + foo := common.BytesToAddress([]byte(encValue.(string))) + fmt.Println(foo) + for _, _byte := range common.BytesToAddress([]byte(encValue.(string))) { + bytesValue = append(bytesValue, _byte) + } + primitiveEncValue = bytesValue + break + case "bool": + primitiveEncType = "uint256" + var int64Val int64 + if encValue.(bool) { + int64Val = 1 + } + primitiveEncValue = abi.U256(big.NewInt(int64Val)) + break + case "bytes", "string": + primitiveEncType = "bytes32" + primitiveEncValue = crypto.Keccak256(bytesValueOf(encValue)) + break + default: + if strings.HasPrefix(encType, "bytes") { + encTypes = append(encTypes, "bytes32") + sizeStr := strings.TrimPrefix(encType, "bytes") + size, _ := strconv.Atoi(sizeStr) + bytesValue := []byte{} + for i := 0; i < 32-size; i++ { + bytesValue = append(bytesValue, 0) + } + for _, _byte := range encValue.([]byte) { + bytesValue = append(bytesValue, _byte) + } + primitiveEncValue = bytesValue + } else if strings.HasPrefix(encType, "uint") || strings.HasPrefix(encType, "int") { + primitiveEncType = "uint256" + primitiveEncValue = abi.U256(encValue.(*big.Int)) + } + break } - return buffer.Bytes() + return primitiveEncType, primitiveEncValue } - // TODO regex - // handle bytes - if strings.Contains(dataType, TypeBytes) { - bytesVal := data.([]byte) - encoding := crypto.Keccak256(bytesVal) - buffer.Write(encoding) - } - - // TODO regex - // handle ints - if strings.Contains(dataType, TypeInt) { - encoding := abi.U256(data.(*big.Int)) // not sure if this is big endian order, but it's definitey sign extended to 256 bit because of using the U256 function - buffer.Write(encoding) - return buffer.Bytes() - } - - // handle what's left - switch dataType { - case TypeAddress: - addressVal, _ := data.(common.Address) - encoding := addressVal.Bytes() // hopefully this means uint160 encoding? - buffer.Write(encoding) - break - case TypeBool: - boolVal, _ := data.(bool) - var int64Val int64 - if boolVal { - int64Val = 1 + // Add field contents. Structs and arrays have special handlings. + for _, field := range typedData.Types[primaryType] { + encType := field["type"] + encValue := data[field["name"]] + if encType[len(encType)-1:] == "]" { + encTypes = append(encTypes, "bytes32") + parsedType := strings.Split(encType, "[")[0] + arrayBuffer := bytes.Buffer{} + for _, item := range encValue.([]interface{}) { + if typedData.Types[parsedType] != nil { + encoding := typedData.EncodeData(parsedType, item.(map[string]interface{})) + arrayBuffer.Write(encoding) + } else { + _, encValue := handlePrimitiveValue(encType, encValue) + arrayBuffer.Write(bytesValueOf(encValue)) + } + } + encValues = append(encValues, crypto.Keccak256(arrayBuffer.Bytes())) + } else if typedData.Types[field["type"]] != nil { + encTypes = append(encTypes, "bytes32") + mapValue := encValue.(map[string]interface{}) + encValue = crypto.Keccak256(typedData.EncodeData(field["type"], mapValue)) + encValues = append(encValues, encValue) + } else { + primitiveEncType, primitiveEncValue := handlePrimitiveValue(encType, encValue) + encTypes = append(encTypes, primitiveEncType) + encValues = append(encValues, primitiveEncValue) } - encoding := abi.U256(big.NewInt(int64Val)) - buffer.Write(encoding) - break - case TypeString: - bytesVal := common.FromHex(data.(string)) - encoding := crypto.Keccak256(bytesVal) - buffer.Write(encoding) - break - default: - break } - printJson("encodeData", map[string]interface{}{ - "dataType": dataType, - "data": data, - "depth": depth, - "encoding": buffer.String(), - }) - return buffer.Bytes() + buffer := bytes.Buffer{} + for _, encValue := range encValues { + buffer.Write(bytesValueOf(encValue)) + } + + return buffer.Bytes() // https://github.com/ethereumjs/ethereumjs-abi/blob/master/lib/index.js#L336 } // Determines the content type and then recovers the address associated with the given sig @@ -488,39 +480,6 @@ func (api *SignerAPI) EcRecover(ctx context.Context, contentType string, data he } } -// sortByPriorityAndName is a helper function to sort types by priority and name. Priority is calculated b -// based upon the number of references. -func sortByPriorityAndName(input map[string]uint) []EIP712TypePriority { - var priorities []EIP712TypePriority - for key, val := range input { - priorities = append(priorities, EIP712TypePriority{key, val}) - } - // Alphabetically - sort.Slice(priorities, func(i, j int) bool { - return priorities[i].Type < priorities[j].Type - }) - // Priority - sort.Slice(priorities, func(i, j int) bool { - return priorities[i].Value > priorities[j].Value - }) - - return priorities -} - -// findNextDataType -// blah blah -func findNextDataType(_types EIP712Types, mapType string, mapKey string) string { - eip712type := _types[mapType] - - for _, mapObj := range eip712type { - if mapObj["name"] == mapKey { - return mapObj["type"] - } - } - - return "" -} - // UnmarshalJSON validates the input data func (typedData *TypedData) UnmarshalJSON(data []byte) error { type input struct { @@ -591,6 +550,7 @@ func (types *EIP712Types) IsValid() error { return fmt.Errorf("referenced type %s is undefined", typeVal) } } else { + // TODO: better type checking if !isStandardTypeStr(typeVal) { if (*types)[typeVal] != nil { return fmt.Errorf("custom type %s must be capitalized", typeVal) diff --git a/signer/core/signed_data_test.go b/signer/core/signed_data_test.go index 6761231c52..80e59872b1 100644 --- a/signer/core/signed_data_test.go +++ b/signer/core/signed_data_test.go @@ -18,17 +18,14 @@ package core import ( "context" - "encoding/json" "fmt" - "math/big" - "testing" - "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" + "math/big" + "testing" ) -var typesStandard = Types{ +var typesStandard = EIP712Types{ "EIP712Domain": { { "name": "name", @@ -73,89 +70,17 @@ var typesStandard = Types{ }, } -var jsonTypedData = ` - { - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Person": [ - { - "name": "name", - "type": "string" - }, - { - "name": "test", - "type": "uint8" - }, - { - "name": "wallet", - "type": "address" - } - ], - "Mail": [ - { - "name": "from", - "type": "Person" - }, - { - "name": "to", - "type": "Person" - }, - { - "name": "contents", - "type": "string" - } - ] - }, - "primaryType": "Mail", - "domain": { - "name": "Ether Mail", - "version": "1", - "chainId": 1, - "verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "from": { - "name": "Cow", - "test": 3, - "wallet": "0xcD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" - }, - "to": { - "name": "Bob", - "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" - }, - "contents": "Hello, Bob!" - } - } -` - const primaryType = "Mail" -var domainStandard = TypedDataDomain{ +var domainStandard = EIP712Domain{ "Ether Mail", "1", big.NewInt(1), "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", - "", + nil, } -var messageStandard = map[string]interface{}{ +var dataStandard = map[string]interface{}{ "from": map[string]interface{}{ "name": "Cow", "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", @@ -168,10 +93,10 @@ var messageStandard = map[string]interface{}{ } var typedData = TypedData{ - Types: typesStandard, - PrimaryType: primaryType, - Domain: domainStandard, - Message: messageStandard, + typesStandard, + primaryType, + domainStandard, + dataStandard, } func TestSignData(t *testing.T) { @@ -188,7 +113,7 @@ func TestSignData(t *testing.T) { control <- "Y" control <- "wrongpassword" - signature, err := api.SignData(context.Background(), TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world"))) + signature, err := api.SignData(context.Background(), TextPlain.Mime, a, []byte("EHLO world")) if signature != nil { t.Errorf("Expected nil-data, got %x", signature) } @@ -196,7 +121,7 @@ func TestSignData(t *testing.T) { t.Errorf("Expected ErrLocked! %v", err) } control <- "No way" - signature, err = api.SignData(context.Background(), TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world"))) + signature, err = api.SignData(context.Background(), TextPlain.Mime, a, []byte("EHLO world")) if signature != nil { t.Errorf("Expected nil-data, got %x", signature) } @@ -206,7 +131,7 @@ func TestSignData(t *testing.T) { // text/plain control <- "Y" control <- "a_long_password" - signature, err = api.SignData(context.Background(), TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world"))) + signature, err = api.SignData(context.Background(), TextPlain.Mime, a, []byte("EHLO world")) if err != nil { t.Fatal(err) } @@ -223,378 +148,44 @@ func TestSignData(t *testing.T) { if signature == nil || len(signature) != 65 { t.Errorf("Expected 65 byte signature (got %d bytes)", len(signature)) } + // TODO: test signature r,s,v values } func TestHashStruct(t *testing.T) { - hash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) - if err != nil { - t.Fatal(err) - } - mainHash := fmt.Sprintf("0x%s", common.Bytes2Hex(hash)) + mainHash := fmt.Sprintf("0x%s", common.Bytes2Hex(typedData.HashStruct(typedData.PrimaryType, typedData.Message))) if mainHash != "0xc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e" { - t.Errorf("Expected different hashStruct result (got %s)", mainHash) + t.Fatal(fmt.Errorf("hashStruct result %s is incorrect", mainHash)) } - hash, err = typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) - if err != nil { - t.Error(err) - } - domainHash := fmt.Sprintf("0x%s", common.Bytes2Hex(hash)) + domainHash := fmt.Sprintf("0x%s", common.Bytes2Hex(typedData.HashStruct("EIP712Domain", typedData.Domain.Map()))) if domainHash != "0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f" { - t.Errorf("Expected different domain hashStruct result (got %s)", domainHash) + t.Fatal(fmt.Errorf("hashStruct result %s is incorrect", domainHash)) } } func TestEncodeType(t *testing.T) { domainTypeEncoding := string(typedData.EncodeType("EIP712Domain")) if domainTypeEncoding != "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" { - t.Errorf("Expected different encodeType result (got %s)", domainTypeEncoding) + t.Fatal(fmt.Errorf("encodeType result %s is incorrect", domainTypeEncoding)) } mailTypeEncoding := string(typedData.EncodeType(typedData.PrimaryType)) if mailTypeEncoding != "Mail(Person from,Person to,string contents)Person(string name,address wallet)" { - t.Errorf("Expected different encodeType result (got %s)", mailTypeEncoding) + t.Fatal(fmt.Errorf("encodeType result %s is incorrect", mailTypeEncoding)) } } func TestTypeHash(t *testing.T) { mailTypeHash := fmt.Sprintf("0x%s", common.Bytes2Hex(typedData.TypeHash(typedData.PrimaryType))) if mailTypeHash != "0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2" { - t.Errorf("Expected different typeHash result (got %s)", mailTypeHash) + t.Fatal(fmt.Errorf("typeHash result %s is incorrect", mailTypeHash)) } } func TestEncodeData(t *testing.T) { - hash, err := typedData.EncodeData(typedData.PrimaryType, typedData.Message, 0) - if err != nil { - t.Fatal(err) - } - dataEncoding := fmt.Sprintf("0x%s", common.Bytes2Hex(hash)) + + dataEncoding := fmt.Sprintf("0x%s", common.Bytes2Hex(typedData.EncodeData(typedData.PrimaryType, typedData.Message))) if dataEncoding != "0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8" { - t.Errorf("Expected different encodeData result (got %s)", dataEncoding) - } -} - -func TestMalformedData1(t *testing.T) { - // Verifies that malformed domain keys are properly caught: - //{ - // "name": "Ether Mail", - // "version": "1", - // "chainId": 1, - // "vxerifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - //} - var jsonTypedData = ` - { - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Person": [ - { - "name": "name", - "type": "string" - }, - { - "name": "wallet", - "type": "address" - } - ], - "Mail": [ - { - "name": "from", - "type": "Person" - }, - { - "name": "to", - "type": "Person" - }, - { - "name": "contents", - "type": "string" - } - ] - }, - "primaryType": "Mail", - "domain": { - "name": "Ether Mail", - "version": "1", - "chainId": 1, - "vxerifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "from": { - "name": "Cow", - "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" - }, - "to": { - "name": "Bob", - "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" - }, - "contents": "Hello, Bob!" - } - } - -` - var malformedTypedData TypedData - err := json.Unmarshal([]byte(jsonTypedData), &malformedTypedData) - if err != nil { - t.Fatalf("unmarshalling failed %v", err) - } - err = malformedTypedData.Validate() - if err != nil { - t.Fatalf("Expected no error, got %v", err) - } - _, err = malformedTypedData.HashStruct("EIP712Domain", malformedTypedData.Domain.Map()) - if err == nil || err.Error() != "provided data '' doesn't match type 'address'" { - t.Errorf("Expected `provided data '' doesn't match type 'address'`, got %v", err) - } -} - -func TestMalformedData2(t *testing.T) { - // Verifies that: - // 1. Mismatches between the given type and data, i.e. `Person` and - // the data item is a string, are properly caught: - //{ - // "name": "contents", - // "type": "Person" - //}, - //{ - // "contents": "Hello, Bob!" <-- string not "Person" - //} - // 2. Nonexistent types are properly caught: - //{ - // "name": "contents", - // "type": "Blahonga" - //} - jsonTypedData := ` - { - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Person": [ - { - "name": "name", - "type": "string" - }, - { - "name": "wallet", - "type": "address" - } - ], - "Mail": [ - { - "name": "from", - "type": "Person" - }, - { - "name": "to", - "type": "Person" - }, - { - "name": "contents", - "type": "Person" - } - ] - }, - "primaryType": "Mail", - "domain": { - "name": "Ether Mail", - "version": "1", - "chainId": 1, - "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "from": { - "name": "Cow", - "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" - }, - "to": { - "name": "Bob", - "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" - }, - "contents": "Hello, Bob!" - } - } -` - var malformedTypedData TypedData - err := json.Unmarshal([]byte(jsonTypedData), &malformedTypedData) - if err != nil { - t.Fatalf("unmarshalling failed %v", err) - } - err = malformedTypedData.Validate() - if err != nil { - t.Fatalf("Expected no error, got %v", err) - } - _, err = malformedTypedData.HashStruct(malformedTypedData.PrimaryType, malformedTypedData.Message) - if err.Error() != "provided data 'Hello, Bob!' doesn't match type 'Person'" { - t.Errorf("Expected `provided data 'Hello, Bob!' doesn't match type 'Person'`, got %v", err) - } - - malformedTypedData.Types["Mail"][2]["type"] = "Blahonga" - err = malformedTypedData.Validate() - if err == nil || err.Error() != "referenced type 'Blahonga' is undefined" { - t.Fatalf("Expected `referenced type 'Blahonga' is undefined`, got %v", err) - } - _, err = malformedTypedData.HashStruct(malformedTypedData.PrimaryType, malformedTypedData.Message) - if err == nil || err.Error() != "unrecognized type 'Blahonga'" { - t.Errorf("Expected `unrecognized type 'Blahonga'`, got %v", err) - } -} - -func TestMalformedData3(t *testing.T) { - // Verifies several quirks - // 1. Using dynamic types and only validating the prefix: - //{ - // "name": "chainId", - // "type": "uint256 ... and now for something completely different" - //} - // 2. Extra data in message: - //{ - // "blahonga": "zonk bonk" - //} - jsonTypedData := ` - { - "types": { - "EIP712Domain": [ - { - "name": "name", - "type": "string" - }, - { - "name": "version", - "type": "string" - }, - { - "name": "chainId", - "type": "uint256 ... and now for something completely different" - }, - { - "name": "verifyingContract", - "type": "address" - } - ], - "Person": [ - { - "name": "name", - "type": "string" - }, - { - "name": "wallet", - "type": "address" - } - ], - "Mail": [ - { - "name": "from", - "type": "Person" - }, - { - "name": "to", - "type": "Person" - }, - { - "name": "contents", - "type": "string" - } - ] - }, - "primaryType": "Mail", - "domain": { - "name": "Ether Mail", - "version": "1", - "chainId": 1, - "verifyingContract": "0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message": { - "from": { - "name": "Cow", - "wallet": "0xcD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" - }, - "to": { - "name": "Bob", - "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" - }, - "contents": "Hello, Bob!" - } - } -` - var malformedTypedData TypedData - err := json.Unmarshal([]byte(jsonTypedData), &malformedTypedData) - if err != nil { - t.Fatalf("unmarshalling failed %v", err) - } - err = malformedTypedData.Validate() - if err == nil || err.Error() != "unknown atomic type 'uint256 ... and now for something completely different'" { - t.Fatalf("Expected `unknown atomic type 'uint256 ... and now for something completely different'`, got %v", err) - } - - malformedTypedData.Types["EIP712Domain"][2]["type"] = "uint256" - malformedTypedData.Message["blahonga"] = "zonk bonk" - _, err = malformedTypedData.HashStruct(malformedTypedData.PrimaryType, malformedTypedData.Message) - if err == nil || err.Error() != "there is extra data provided in the message" { - t.Errorf("Expected `there is extra data provided in the message`, got %v", err) - } -} - -func TestMalformedData4(t *testing.T) { - // Verifies data that doesn't fit into it: - //{ - // "test": 65536 <-- test defined as uint8 - //} - var malformedTypedData TypedData - err := json.Unmarshal([]byte(jsonTypedData), &malformedTypedData) - if err != nil { - t.Fatalf("unmarshalling failed %v", err) - } - // Set test to something outside uint8 - (malformedTypedData.Message["from"]).(map[string]interface{})["test"] = 65536 - - err = malformedTypedData.Validate() - if err != nil { - t.Fatalf("Expected no error, got %v", err) - } - - _, err = malformedTypedData.HashStruct(malformedTypedData.PrimaryType, malformedTypedData.Message) - if err == nil || err.Error() != "provided data '65536' doesn't match type 'uint8'" { - t.Fatalf("Expected `provided data '65536' doesn't match type 'uint8'`, got %v", err) - } - - (malformedTypedData.Message["from"]).(map[string]interface{})["test"] = big.NewInt(3) - (malformedTypedData.Message["to"]).(map[string]interface{})["test"] = big.NewInt(4) - - _, err = malformedTypedData.HashStruct(malformedTypedData.PrimaryType, malformedTypedData.Message) - if err != nil { - t.Fatalf("Expected no err, got %v", err) + t.Fatal(fmt.Errorf("encodeData result %s is incorrect", dataEncoding)) } }