aboutsummaryrefslogtreecommitdiffstats
path: root/signer/core
diff options
context:
space:
mode:
authorPaul Berg <hello@paulrberg.com>2019-02-06 15:30:49 +0800
committerMartin Holst Swende <martin@swende.se>2019-02-06 15:30:49 +0800
commit572baae10a28da2d02085df7e2f3a282883f5d6e (patch)
tree3e5424ac64dbeae440604d32aed4293199179c99 /signer/core
parent7c60d0a6a2d3925c2862cbbb188988475619fd0d (diff)
downloadgo-tangerine-572baae10a28da2d02085df7e2f3a282883f5d6e.tar
go-tangerine-572baae10a28da2d02085df7e2f3a282883f5d6e.tar.gz
go-tangerine-572baae10a28da2d02085df7e2f3a282883f5d6e.tar.bz2
go-tangerine-572baae10a28da2d02085df7e2f3a282883f5d6e.tar.lz
go-tangerine-572baae10a28da2d02085df7e2f3a282883f5d6e.tar.xz
go-tangerine-572baae10a28da2d02085df7e2f3a282883f5d6e.tar.zst
go-tangerine-572baae10a28da2d02085df7e2f3a282883f5d6e.zip
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
Diffstat (limited to 'signer/core')
-rw-r--r--signer/core/abihelper.go7
-rw-r--r--signer/core/abihelper_test.go5
-rw-r--r--signer/core/api.go74
-rw-r--r--signer/core/api_test.go39
-rw-r--r--signer/core/auditlog.go27
-rw-r--r--signer/core/cliui.go9
-rw-r--r--signer/core/signed_data.go899
-rw-r--r--signer/core/signed_data_test.go774
-rw-r--r--signer/core/types.go3
9 files changed, 1720 insertions, 117 deletions
diff --git a/signer/core/abihelper.go b/signer/core/abihelper.go
index 0fef24939..de6b815a6 100644
--- a/signer/core/abihelper.go
+++ b/signer/core/abihelper.go
@@ -17,17 +17,16 @@
package core
import (
+ "bytes"
"encoding/json"
"fmt"
"io/ioutil"
+ "os"
+ "regexp"
"strings"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
-
- "bytes"
- "os"
- "regexp"
)
type decodedArgument struct {
diff --git a/signer/core/abihelper_test.go b/signer/core/abihelper_test.go
index 878210be1..4a3a2f06d 100644
--- a/signer/core/abihelper_test.go
+++ b/signer/core/abihelper_test.go
@@ -18,12 +18,11 @@ package core
import (
"fmt"
- "strings"
- "testing"
-
"io/ioutil"
"math/big"
"reflect"
+ "strings"
+ "testing"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
diff --git a/signer/core/api.go b/signer/core/api.go
index e112df9c7..0521dce47 100644
--- a/signer/core/api.go
+++ b/signer/core/api.go
@@ -30,7 +30,6 @@ import (
"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"
@@ -40,9 +39,9 @@ const (
// numberOfAccountsToDerive For hardware wallets, the number of accounts to derive
numberOfAccountsToDerive = 10
// ExternalAPIVersion -- see extapi_changelog.md
- ExternalAPIVersion = "4.0.0"
+ ExternalAPIVersion = "5.0.0"
// InternalAPIVersion -- see intapi_changelog.md
- InternalAPIVersion = "3.0.0"
+ InternalAPIVersion = "3.1.0"
)
// ExternalAPI defines the external API through which signing requests are made.
@@ -53,8 +52,12 @@ type ExternalAPI interface {
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)
+ // SignData - request to sign the given data (plus prefix)
+ SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error)
+ // SignTypedData - request to sign the given structured data (plus prefix)
+ SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data TypedData) (hexutil.Bytes, error)
+ // EcRecover - recover public key from given message and signature
+ EcRecover(ctx context.Context, data hexutil.Bytes, 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
@@ -177,11 +180,12 @@ type (
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"`
+ ContentType string `json:"content_type"`
+ Address common.MixedcaseAddress `json:"address"`
+ Rawdata []byte `json:"raw_data"`
+ Message []*NameValueType `json:"message"`
+ Hash hexutil.Bytes `json:"hash"`
+ Meta Metadata `json:"meta"`
}
SignDataResponse struct {
Approved bool `json:"approved"`
@@ -517,56 +521,6 @@ func (api *SignerAPI) SignTransaction(ctx context.Context, args SendTxArgs, meth
}
-// 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.SignTextWithPassphrase(account, res.Password, data)
- 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
-}
-
-// 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)})
diff --git a/signer/core/api_test.go b/signer/core/api_test.go
index 114470cf9..0e0c54517 100644
--- a/signer/core/api_test.go
+++ b/signer/core/api_test.go
@@ -244,45 +244,6 @@ func TestNewAcc(t *testing.T) {
}
}
-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])
-
- 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 <- "a_long_password"
- 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)
diff --git a/signer/core/auditlog.go b/signer/core/auditlog.go
index 0cb6c9c47..d64ba1ef9 100644
--- a/signer/core/auditlog.go
+++ b/signer/core/auditlog.go
@@ -18,7 +18,6 @@ package core
import (
"context"
-
"encoding/json"
"github.com/ethereum/go-ethereum/accounts"
@@ -63,11 +62,27 @@ func (l *AuditLogger) SignTransaction(ctx context.Context, args SendTxArgs, meth
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)
+func (l *AuditLogger) SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error) {
+ l.log.Info("SignData", "type", "request", "metadata", MetadataFromContext(ctx).String(),
+ "addr", addr.String(), "data", data, "content-type", contentType)
+ b, e := l.api.SignData(ctx, contentType, addr, data)
+ l.log.Info("SignData", "type", "response", "data", common.Bytes2Hex(b), "error", e)
+ return b, e
+}
+
+func (l *AuditLogger) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data TypedData) (hexutil.Bytes, error) {
+ l.log.Info("SignTypedData", "type", "request", "metadata", MetadataFromContext(ctx).String(),
+ "addr", addr.String(), "data", data)
+ b, e := l.api.SignTypedData(ctx, addr, data)
+ l.log.Info("SignTypedData", "type", "response", "data", common.Bytes2Hex(b), "error", e)
+ return b, e
+}
+
+func (l *AuditLogger) EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) {
+ l.log.Info("EcRecover", "type", "request", "metadata", MetadataFromContext(ctx).String(),
+ "data", common.Bytes2Hex(data), "sig", common.Bytes2Hex(sig))
+ b, e := l.api.EcRecover(ctx, data, sig)
+ l.log.Info("EcRecover", "type", "response", "address", b.String(), "error", e)
return b, e
}
diff --git a/signer/core/cliui.go b/signer/core/cliui.go
index 940f1f43a..71d489d45 100644
--- a/signer/core/cliui.go
+++ b/signer/core/cliui.go
@@ -21,7 +21,6 @@ import (
"fmt"
"os"
"strings"
-
"sync"
"github.com/davecgh/go-spew/spew"
@@ -165,8 +164,12 @@ func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResp
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:\n")
+ for _, nvt := range request.Message {
+ fmt.Printf("%v\n", nvt.Pprint(1))
+ }
+ //fmt.Printf("message: \n%v\n", request.Message)
+ fmt.Printf("raw data: \n%q\n", request.Rawdata)
fmt.Printf("message hash: %v\n", request.Hash)
fmt.Printf("-------------------------------------------\n")
showMetadata(request.Meta)
diff --git a/signer/core/signed_data.go b/signer/core/signed_data.go
new file mode 100644
index 000000000..ac0b97bca
--- /dev/null
+++ b/signer/core/signed_data.go
@@ -0,0 +1,899 @@
+// 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"
+ "math/big"
+ "mime"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+ "unicode"
+
+ "github.com/ethereum/go-ethereum/accounts"
+ "github.com/ethereum/go-ethereum/accounts/abi"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+ "github.com/ethereum/go-ethereum/common/math"
+ "github.com/ethereum/go-ethereum/consensus/clique"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/rlp"
+)
+
+type SigFormat struct {
+ Mime string
+ ByteVersion byte
+}
+
+var (
+ TextValidator = SigFormat{
+ accounts.MimetypeTextWithValidator,
+ 0x00,
+ }
+ DataTyped = SigFormat{
+ accounts.MimetypeTypedData,
+ 0x01,
+ }
+ ApplicationClique = SigFormat{
+ accounts.MimetypeClique,
+ 0x02,
+ }
+ TextPlain = SigFormat{
+ accounts.MimetypeTextPlain,
+ 0x45,
+ }
+)
+
+type ValidatorData struct {
+ Address common.Address
+ Message hexutil.Bytes
+}
+
+type TypedData struct {
+ Types Types `json:"types"`
+ PrimaryType string `json:"primaryType"`
+ Domain TypedDataDomain `json:"domain"`
+ Message TypedDataMessage `json:"message"`
+}
+
+type Type struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+}
+
+func (t *Type) isArray() bool {
+ return strings.HasSuffix(t.Type, "[]")
+}
+
+// typeName returns the canonical name of the type. If the type is 'Person[]', then
+// this method returns 'Person'
+func (t *Type) typeName() string {
+ if strings.HasSuffix(t.Type, "[]") {
+ return strings.TrimSuffix(t.Type, "[]")
+ }
+ return t.Type
+}
+
+func (t *Type) isReferenceType() bool {
+ // Reference types must have a leading uppercase characer
+ return unicode.IsUpper([]rune(t.Type)[0])
+}
+
+type Types map[string][]Type
+
+type TypePriority struct {
+ Type string
+ Value uint
+}
+
+type TypedDataMessage = map[string]interface{}
+
+type TypedDataDomain struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+ ChainId *big.Int `json:"chainId"`
+ VerifyingContract string `json:"verifyingContract"`
+ Salt string `json:"salt"`
+}
+
+var typedDataReferenceTypeRegexp = regexp.MustCompile(`^[A-Z](\w*)(\[\])?$`)
+
+// sign receives a request and produces a signature
+
+// 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.
+func (api *SignerAPI) sign(addr common.MixedcaseAddress, req *SignDataRequest) (hexutil.Bytes, error) {
+
+ // We make the request prior to looking up if we actually have the account, to prevent
+ // account-enumeration via the API
+ 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
+ }
+ // Sign the data with the wallet
+ signature, err := wallet.SignDataWithPassphrase(account, res.Password, req.ContentType, req.Hash)
+ if err != nil {
+ return nil, err
+ }
+ signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
+ return signature, nil
+}
+
+// SignData signs the hash of the provided data, but does so differently
+// depending on the content-type specified.
+//
+// Different types of validation occur.
+func (api *SignerAPI) SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error) {
+ var req, err = api.determineSignatureFormat(ctx, contentType, addr, data)
+ if err != nil {
+ return nil, err
+ }
+
+ signature, err := api.sign(addr, req)
+ if err != nil {
+ api.UI.ShowError(err.Error())
+ return nil, err
+ }
+
+ return signature, nil
+}
+
+// determineSignatureFormat determines which signature method should be used based upon the mime type
+// In the cases where it matters ensure that the charset is handled. The charset
+// resides in the 'params' returned as the second returnvalue from mime.ParseMediaType
+// charset, ok := params["charset"]
+// As it is now, we accept any charset and just treat it as 'raw'.
+// This method returns the mimetype for signing along with the request
+func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (*SignDataRequest, error) {
+ var req *SignDataRequest
+
+ mediaType, _, err := mime.ParseMediaType(contentType)
+ if err != nil {
+ return nil, err
+ }
+
+ switch mediaType {
+ case TextValidator.Mime:
+ // Data with an intended validator
+ validatorData, err := UnmarshalValidatorData(data)
+ if err != nil {
+ return nil, err
+ }
+ sighash, msg := SignTextValidator(validatorData)
+ message := []*NameValueType{
+ {
+ Name: "message",
+ Typ: "text",
+ Value: msg,
+ },
+ }
+ req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Message: message, Hash: sighash}
+ case ApplicationClique.Mime:
+ // Clique is the Ethereum PoA standard
+ stringData, ok := data.(string)
+ if !ok {
+ return nil, fmt.Errorf("input for %v plain must be an hex-encoded string", ApplicationClique.Mime)
+ }
+ cliqueData, err := hexutil.Decode(stringData)
+ if err != nil {
+ return nil, err
+ }
+ header := &types.Header{}
+ if err := rlp.DecodeBytes(cliqueData, header); err != nil {
+ return nil, err
+ }
+ // Get back the rlp data, encoded by us
+ cliqueData = clique.CliqueRLP(header)
+ sighash, err := SignCliqueHeader(header)
+ if err != nil {
+ return nil, err
+ }
+ message := []*NameValueType{
+ {
+ Name: "Clique block",
+ Typ: "clique",
+ Value: fmt.Sprintf("clique block %d [0x%x]", header.Number, header.Hash()),
+ },
+ }
+ req = &SignDataRequest{ContentType: mediaType, Rawdata: cliqueData, Message: message, Hash: sighash}
+ default: // also case TextPlain.Mime:
+ // Calculates an Ethereum ECDSA signature for:
+ // hash = keccak256("\x19${byteVersion}Ethereum Signed Message:\n${message length}${message}")
+ // We expect it to be a string
+ stringData, ok := data.(string)
+ if !ok {
+ return nil, fmt.Errorf("input for text/plain must be a string")
+ }
+ //plainData, err := hexutil.Decode(stringdata)
+ //if err != nil {
+ // return nil, err
+ //}
+ sighash, msg := accounts.TextAndHash([]byte(stringData))
+ message := []*NameValueType{
+ {
+ Name: "message",
+ Typ: "text/plain",
+ Value: msg,
+ },
+ }
+ req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Message: message, Hash: sighash}
+ }
+ req.Address = addr
+ req.Meta = MetadataFromContext(ctx)
+ return req, nil
+
+}
+
+// SignTextWithValidator signs the given message which can be further recovered
+// with the given validator.
+// hash = keccak256("\x19\x00"${address}${data}).
+func SignTextValidator(validatorData ValidatorData) (hexutil.Bytes, string) {
+ msg := fmt.Sprintf("\x19\x00%s%s", string(validatorData.Address.Bytes()), string(validatorData.Message))
+ fmt.Printf("SignTextValidator:%s\n", msg)
+ return crypto.Keccak256([]byte(msg)), msg
+}
+
+// SignCliqueHeader returns the hash which is used as input for the proof-of-authority
+// signing. It is the hash of the entire header apart from the 65 byte signature
+// contained at the end of the extra data.
+//
+// The method requires the extra data to be at least 65 bytes -- the original implementation
+// in clique.go panics if this is the case, thus it's been reimplemented here to avoid the panic
+// and simply return an error instead
+func SignCliqueHeader(header *types.Header) (hexutil.Bytes, error) {
+ //hash := common.Hash{}
+ if len(header.Extra) < 65 {
+ return nil, fmt.Errorf("clique header extradata too short, %d < 65", len(header.Extra))
+ }
+ hash := clique.SealHash(header)
+ return hash.Bytes(), nil
+}
+
+// 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) {
+ domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map())
+ if err != nil {
+ return nil, err
+ }
+ typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message)
+ if err != nil {
+ return nil, err
+ }
+ rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash)))
+ sighash := crypto.Keccak256(rawData)
+ message := typedData.Format()
+ req := &SignDataRequest{ContentType: DataTyped.Mime, Rawdata: rawData, Message: message, Hash: sighash}
+ signature, err := api.sign(addr, req)
+ if err != nil {
+ api.UI.ShowError(err.Error())
+ return nil, err
+ }
+ return signature, nil
+}
+
+// HashStruct generates a keccak256 hash of the encoding of the provided data
+func (typedData *TypedData) HashStruct(primaryType string, data TypedDataMessage) (hexutil.Bytes, error) {
+ encodedData, err := typedData.EncodeData(primaryType, data, 1)
+ if err != nil {
+ return nil, err
+ }
+ return crypto.Keccak256(encodedData), nil
+}
+
+// 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 == str {
+ return true
+ }
+ }
+ return false
+ }
+
+ 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)
+ }
+ }
+ }
+ return found
+}
+
+// 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) hexutil.Bytes {
+ // Get dependencies primary first, then alphabetical
+ deps := typedData.Dependencies(primaryType, []string{})
+ slicedDeps := deps[1:]
+ sort.Strings(slicedDeps)
+ deps = append([]string{primaryType}, slicedDeps...)
+
+ // Format as a string with fields
+ var buffer bytes.Buffer
+ for _, dep := range deps {
+ buffer.WriteString(dep)
+ buffer.WriteString("(")
+ for _, obj := range typedData.Types[dep] {
+ buffer.WriteString(obj.Type)
+ buffer.WriteString(" ")
+ buffer.WriteString(obj.Name)
+ buffer.WriteString(",")
+ }
+ buffer.Truncate(buffer.Len() - 1)
+ buffer.WriteString(")")
+ }
+ return buffer.Bytes()
+}
+
+// TypeHash creates the keccak256 hash of the data
+func (typedData *TypedData) TypeHash(primaryType string) hexutil.Bytes {
+ return crypto.Keccak256(typedData.EncodeType(primaryType))
+}
+
+// EncodeData generates the following encoding:
+// `enc(value₁) ‖ enc(value₂) ‖ … ‖ enc(valueₙ)`
+//
+// each encoded member is 32-byte long
+func (typedData *TypedData) EncodeData(primaryType string, data map[string]interface{}, depth int) (hexutil.Bytes, error) {
+ if err := typedData.validate(); err != nil {
+ return nil, err
+ }
+
+ buffer := bytes.Buffer{}
+
+ // Verify extra data
+ if len(typedData.Types[primaryType]) < len(data) {
+ return nil, errors.New("there is extra data provided in the message")
+ }
+
+ // Add typehash
+ buffer.Write(typedData.TypeHash(primaryType))
+
+ // Add field contents. Structs and arrays have special handlers.
+ for _, field := range typedData.Types[primaryType] {
+ encType := field.Type
+ encValue := data[field.Name]
+ if encType[len(encType)-1:] == "]" {
+ arrayValue, ok := encValue.([]interface{})
+ if !ok {
+ return nil, dataMismatchError(encType, encValue)
+ }
+
+ arrayBuffer := bytes.Buffer{}
+ parsedType := strings.Split(encType, "[")[0]
+ for _, item := range arrayValue {
+ if typedData.Types[parsedType] != nil {
+ mapValue, ok := item.(map[string]interface{})
+ if !ok {
+ return nil, dataMismatchError(parsedType, item)
+ }
+ encodedData, err := typedData.EncodeData(parsedType, mapValue, depth+1)
+ if err != nil {
+ return nil, err
+ }
+ arrayBuffer.Write(encodedData)
+ } else {
+ bytesValue, err := typedData.EncodePrimitiveValue(parsedType, item, depth)
+ if err != nil {
+ return nil, err
+ }
+ arrayBuffer.Write(bytesValue)
+ }
+ }
+
+ buffer.Write(crypto.Keccak256(arrayBuffer.Bytes()))
+ } else if typedData.Types[field.Type] != nil {
+ mapValue, ok := encValue.(map[string]interface{})
+ if !ok {
+ return nil, dataMismatchError(encType, encValue)
+ }
+ encodedData, err := typedData.EncodeData(field.Type, mapValue, depth+1)
+ if err != nil {
+ return nil, err
+ }
+ buffer.Write(crypto.Keccak256(encodedData))
+ } else {
+ byteValue, err := typedData.EncodePrimitiveValue(encType, encValue, depth)
+ if err != nil {
+ return nil, err
+ }
+ buffer.Write(byteValue)
+ }
+ }
+ return buffer.Bytes(), nil
+}
+
+// EncodePrimitiveValue deals with the primitive values found
+// while searching through the typed data
+func (typedData *TypedData) EncodePrimitiveValue(encType string, encValue interface{}, depth int) ([]byte, error) {
+
+ switch encType {
+ case "address":
+ stringValue, ok := encValue.(string)
+ if !ok || !common.IsHexAddress(stringValue) {
+ return nil, dataMismatchError(encType, encValue)
+ }
+ retval := make([]byte, 32)
+ copy(retval[12:], common.HexToAddress(stringValue).Bytes())
+ return retval, nil
+ case "bool":
+ boolValue, ok := encValue.(bool)
+ if !ok {
+ return nil, dataMismatchError(encType, encValue)
+ }
+ if boolValue {
+ return math.PaddedBigBytes(common.Big1, 32), nil
+ }
+ return math.PaddedBigBytes(common.Big0, 32), nil
+ case "string":
+ strVal, ok := encValue.(string)
+ if !ok {
+ return nil, dataMismatchError(encType, encValue)
+ }
+ return crypto.Keccak256([]byte(strVal)), nil
+ case "bytes":
+ bytesValue, ok := encValue.([]byte)
+ if !ok {
+ return nil, dataMismatchError(encType, encValue)
+ }
+ return crypto.Keccak256(bytesValue), nil
+ }
+ if strings.HasPrefix(encType, "bytes") {
+ lengthStr := strings.TrimPrefix(encType, "bytes")
+ length, err := strconv.Atoi(lengthStr)
+ if err != nil {
+ return nil, fmt.Errorf("invalid size on bytes: %v", lengthStr)
+ }
+ if length < 0 || length > 32 {
+ return nil, fmt.Errorf("invalid size on bytes: %d", length)
+ }
+ if byteValue, ok := encValue.(hexutil.Bytes); !ok {
+ return nil, dataMismatchError(encType, encValue)
+ } else {
+ return math.PaddedBigBytes(new(big.Int).SetBytes(byteValue), 32), nil
+ }
+ }
+ if strings.HasPrefix(encType, "int") || strings.HasPrefix(encType, "uint") {
+ length := 0
+ if encType == "int" || encType == "uint" {
+ length = 256
+ } else {
+ lengthStr := ""
+ if strings.HasPrefix(encType, "uint") {
+ lengthStr = strings.TrimPrefix(encType, "uint")
+ } else {
+ lengthStr = strings.TrimPrefix(encType, "int")
+ }
+ atoiSize, err := strconv.Atoi(lengthStr)
+ if err != nil {
+ return nil, fmt.Errorf("invalid size on integer: %v", lengthStr)
+ }
+ length = atoiSize
+ }
+ bigIntValue, ok := encValue.(*big.Int)
+ if bigIntValue.BitLen() > length {
+ return nil, fmt.Errorf("integer larger than '%v'", encType)
+ }
+ if !ok {
+ return nil, dataMismatchError(encType, encValue)
+ }
+ return abi.U256(bigIntValue), nil
+ }
+ return nil, fmt.Errorf("unrecognized type '%s'", encType)
+
+}
+
+// dataMismatchError generates an error for a mismatch between
+// the provided type and data
+func dataMismatchError(encType string, encValue interface{}) error {
+ return fmt.Errorf("provided data '%v' doesn't match type '%s'", encValue, encType)
+}
+
+// EcRecover recovers the address associated with the given sig.
+// Only compatible with `text/plain`
+func (api *SignerAPI) EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) {
+ // 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("\x19${byteVersion}Ethereum 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
+ 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 := accounts.TextHash(data)
+ rpk, err := crypto.SigToPub(hash, sig)
+ if err != nil {
+ return common.Address{}, err
+ }
+ return crypto.PubkeyToAddress(*rpk), nil
+}
+
+// UnmarshalValidatorData converts the bytes input to typed data
+func UnmarshalValidatorData(data interface{}) (ValidatorData, error) {
+ raw, ok := data.(map[string]interface{})
+ if !ok {
+ return ValidatorData{}, errors.New("validator input is not a map[string]interface{}")
+ }
+ addr, ok := raw["address"].(string)
+ if !ok {
+ return ValidatorData{}, errors.New("validator address is not sent as a string")
+ }
+ addrBytes, err := hexutil.Decode(addr)
+ if err != nil {
+ return ValidatorData{}, err
+ }
+ if !ok || len(addrBytes) == 0 {
+ return ValidatorData{}, errors.New("validator address is undefined")
+ }
+
+ message, ok := raw["message"].(string)
+ if !ok {
+ return ValidatorData{}, errors.New("message is not sent as a string")
+ }
+ messageBytes, err := hexutil.Decode(message)
+ if err != nil {
+ return ValidatorData{}, err
+ }
+ if !ok || len(messageBytes) == 0 {
+ return ValidatorData{}, errors.New("message is undefined")
+ }
+
+ return ValidatorData{
+ Address: common.BytesToAddress(addrBytes),
+ Message: messageBytes,
+ }, nil
+}
+
+// validate makes sure the types are sound
+func (typedData *TypedData) validate() error {
+ if err := typedData.Types.validate(); err != nil {
+ return err
+ }
+ if err := typedData.Domain.validate(); err != nil {
+ return err
+ }
+ return nil
+}
+
+// Map generates a map version of the typed data
+func (typedData *TypedData) Map() map[string]interface{} {
+ dataMap := map[string]interface{}{
+ "types": typedData.Types,
+ "domain": typedData.Domain.Map(),
+ "primaryType": typedData.PrimaryType,
+ "message": typedData.Message,
+ }
+ return dataMap
+}
+
+// PrettyPrint generates a nice output to help the users
+// of clef present data in their apps
+func (typedData *TypedData) PrettyPrint() string {
+ output := bytes.Buffer{}
+ formatted := typedData.Format()
+ for _, item := range formatted {
+ output.WriteString(fmt.Sprintf("%v\n", item.Pprint(0)))
+ }
+ return output.String()
+}
+
+// Format returns a representation of typedData, which can be easily displayed by a user-interface
+// without in-depth knowledge about 712 rules
+func (typedData *TypedData) Format() []*NameValueType {
+ var nvts []*NameValueType
+ nvts = append(nvts, &NameValueType{
+ Name: "EIP712Domain",
+ Value: typedData.formatData("EIP712Domain", typedData.Domain.Map()),
+ Typ: "domain",
+ })
+ nvts = append(nvts, &NameValueType{
+ Name: typedData.PrimaryType,
+ Value: typedData.formatData(typedData.PrimaryType, typedData.Message),
+ Typ: "primary type",
+ })
+ return nvts
+}
+
+func (typedData *TypedData) formatData(primaryType string, data map[string]interface{}) []*NameValueType {
+ var output []*NameValueType
+
+ // Add field contents. Structs and arrays have special handlers.
+ for _, field := range typedData.Types[primaryType] {
+ encName := field.Name
+ encValue := data[encName]
+ item := &NameValueType{
+ Name: encName,
+ Typ: field.Type,
+ }
+ if field.isArray() {
+ arrayValue, _ := encValue.([]interface{})
+ parsedType := field.typeName()
+ for _, v := range arrayValue {
+ if typedData.Types[parsedType] != nil {
+ mapValue, _ := v.(map[string]interface{})
+ mapOutput := typedData.formatData(parsedType, mapValue)
+ item.Value = mapOutput
+ } else {
+ primitiveOutput := formatPrimitiveValue(field.Type, encValue)
+ item.Value = primitiveOutput
+ }
+ }
+ } else if typedData.Types[field.Type] != nil {
+ mapValue, _ := encValue.(map[string]interface{})
+ mapOutput := typedData.formatData(field.Type, mapValue)
+ item.Value = mapOutput
+ } else {
+ primitiveOutput := formatPrimitiveValue(field.Type, encValue)
+ item.Value = primitiveOutput
+ }
+ output = append(output, item)
+ }
+ return output
+}
+
+func formatPrimitiveValue(encType string, encValue interface{}) string {
+ switch encType {
+ case "address":
+ stringValue, _ := encValue.(string)
+ return common.HexToAddress(stringValue).String()
+ case "bool":
+ boolValue, _ := encValue.(bool)
+ return fmt.Sprintf("%t", boolValue)
+ case "bytes", "string":
+ return fmt.Sprintf("%s", encValue)
+ }
+ if strings.HasPrefix(encType, "bytes") {
+ return fmt.Sprintf("%s", encValue)
+ } else if strings.HasPrefix(encType, "uint") || strings.HasPrefix(encType, "int") {
+ bigIntValue, _ := encValue.(*big.Int)
+ return fmt.Sprintf("%d (0x%x)", bigIntValue, bigIntValue)
+ }
+ return "NA"
+}
+
+// NameValueType is a very simple struct with Name, Value and Type. It's meant for simple
+// json structures used to communicate signing-info about typed data with the UI
+type NameValueType struct {
+ Name string `json:"name"`
+ Value interface{} `json:"value"`
+ Typ string `json:"type"`
+}
+
+// Pprint returns a pretty-printed version of nvt
+func (nvt *NameValueType) Pprint(depth int) string {
+ output := bytes.Buffer{}
+ output.WriteString(strings.Repeat("\u00a0", depth*2))
+ output.WriteString(fmt.Sprintf("%s [%s]: ", nvt.Name, nvt.Typ))
+ if nvts, ok := nvt.Value.([]*NameValueType); ok {
+ output.WriteString("\n")
+ for _, next := range nvts {
+ sublevel := next.Pprint(depth + 1)
+ output.WriteString(sublevel)
+ }
+ } else {
+ output.WriteString(fmt.Sprintf("%s\n", nvt.Value))
+ }
+ return output.String()
+}
+
+// Validate checks if the types object is conformant to the specs
+func (t Types) validate() error {
+ for typeKey, typeArr := range t {
+ for _, typeObj := range typeArr {
+ if typeKey == typeObj.Type {
+ return fmt.Errorf("type '%s' cannot reference itself", typeObj.Type)
+ }
+ if typeObj.isReferenceType() {
+ if _, exist := t[typeObj.Type]; !exist {
+ return fmt.Errorf("reference type '%s' is undefined", typeObj.Type)
+ }
+ if !typedDataReferenceTypeRegexp.MatchString(typeObj.Type) {
+ return fmt.Errorf("unknown reference type '%s", typeObj.Type)
+ }
+ } else if !isPrimitiveTypeValid(typeObj.Type) {
+ return fmt.Errorf("unknown type '%s'", typeObj.Type)
+ }
+ }
+ }
+ return nil
+}
+
+// Checks if the primitive value is valid
+func isPrimitiveTypeValid(primitiveType string) bool {
+ if primitiveType == "address" ||
+ primitiveType == "address[]" ||
+ primitiveType == "bool" ||
+ primitiveType == "bool[]" ||
+ primitiveType == "string" ||
+ primitiveType == "string[]" {
+ return true
+ }
+ if primitiveType == "bytes" ||
+ primitiveType == "bytes[]" ||
+ primitiveType == "bytes1" ||
+ primitiveType == "bytes1[]" ||
+ primitiveType == "bytes2" ||
+ primitiveType == "bytes2[]" ||
+ primitiveType == "bytes3" ||
+ primitiveType == "bytes3[]" ||
+ primitiveType == "bytes4" ||
+ primitiveType == "bytes4[]" ||
+ primitiveType == "bytes5" ||
+ primitiveType == "bytes5[]" ||
+ primitiveType == "bytes6" ||
+ primitiveType == "bytes6[]" ||
+ primitiveType == "bytes7" ||
+ primitiveType == "bytes7[]" ||
+ primitiveType == "bytes8" ||
+ primitiveType == "bytes8[]" ||
+ primitiveType == "bytes9" ||
+ primitiveType == "bytes9[]" ||
+ primitiveType == "bytes10" ||
+ primitiveType == "bytes10[]" ||
+ primitiveType == "bytes11" ||
+ primitiveType == "bytes11[]" ||
+ primitiveType == "bytes12" ||
+ primitiveType == "bytes12[]" ||
+ primitiveType == "bytes13" ||
+ primitiveType == "bytes13[]" ||
+ primitiveType == "bytes14" ||
+ primitiveType == "bytes14[]" ||
+ primitiveType == "bytes15" ||
+ primitiveType == "bytes15[]" ||
+ primitiveType == "bytes16" ||
+ primitiveType == "bytes16[]" ||
+ primitiveType == "bytes17" ||
+ primitiveType == "bytes17[]" ||
+ primitiveType == "bytes18" ||
+ primitiveType == "bytes18[]" ||
+ primitiveType == "bytes19" ||
+ primitiveType == "bytes19[]" ||
+ primitiveType == "bytes20" ||
+ primitiveType == "bytes20[]" ||
+ primitiveType == "bytes21" ||
+ primitiveType == "bytes21[]" ||
+ primitiveType == "bytes22" ||
+ primitiveType == "bytes22[]" ||
+ primitiveType == "bytes23" ||
+ primitiveType == "bytes23[]" ||
+ primitiveType == "bytes24" ||
+ primitiveType == "bytes24[]" ||
+ primitiveType == "bytes25" ||
+ primitiveType == "bytes25[]" ||
+ primitiveType == "bytes26" ||
+ primitiveType == "bytes26[]" ||
+ primitiveType == "bytes27" ||
+ primitiveType == "bytes27[]" ||
+ primitiveType == "bytes28" ||
+ primitiveType == "bytes28[]" ||
+ primitiveType == "bytes29" ||
+ primitiveType == "bytes29[]" ||
+ primitiveType == "bytes30" ||
+ primitiveType == "bytes30[]" ||
+ primitiveType == "bytes31" ||
+ primitiveType == "bytes31[]" {
+ return true
+ }
+ if primitiveType == "int" ||
+ primitiveType == "int[]" ||
+ primitiveType == "int8" ||
+ primitiveType == "int8[]" ||
+ primitiveType == "int16" ||
+ primitiveType == "int16[]" ||
+ primitiveType == "int32" ||
+ primitiveType == "int32[]" ||
+ primitiveType == "int64" ||
+ primitiveType == "int64[]" ||
+ primitiveType == "int128" ||
+ primitiveType == "int128[]" ||
+ primitiveType == "int256" ||
+ primitiveType == "int256[]" {
+ return true
+ }
+ if primitiveType == "uint" ||
+ primitiveType == "uint[]" ||
+ primitiveType == "uint8" ||
+ primitiveType == "uint8[]" ||
+ primitiveType == "uint16" ||
+ primitiveType == "uint16[]" ||
+ primitiveType == "uint32" ||
+ primitiveType == "uint32[]" ||
+ primitiveType == "uint64" ||
+ primitiveType == "uint64[]" ||
+ primitiveType == "uint128" ||
+ primitiveType == "uint128[]" ||
+ primitiveType == "uint256" ||
+ primitiveType == "uint256[]" {
+ return true
+ }
+ return false
+}
+
+// validate checks if the given domain is valid, i.e. contains at least
+// the minimum viable keys and values
+func (domain *TypedDataDomain) validate() error {
+ if domain.ChainId == big.NewInt(0) {
+ return errors.New("chainId must be specified according to EIP-155")
+ }
+
+ if len(domain.Name) == 0 && len(domain.Version) == 0 && len(domain.VerifyingContract) == 0 && len(domain.Salt) == 0 {
+ return errors.New("domain is undefined")
+ }
+
+ return nil
+}
+
+// Map is a helper function to generate a map version of the domain
+func (domain *TypedDataDomain) Map() map[string]interface{} {
+ dataMap := map[string]interface{}{
+ "chainId": domain.ChainId,
+ }
+
+ if len(domain.Name) > 0 {
+ dataMap["name"] = domain.Name
+ }
+
+ if len(domain.Version) > 0 {
+ dataMap["version"] = domain.Version
+ }
+
+ if len(domain.VerifyingContract) > 0 {
+ dataMap["verifyingContract"] = domain.VerifyingContract
+ }
+
+ if len(domain.Salt) > 0 {
+ dataMap["salt"] = domain.Salt
+ }
+ return dataMap
+}
diff --git a/signer/core/signed_data_test.go b/signer/core/signed_data_test.go
new file mode 100644
index 000000000..7d44bce2c
--- /dev/null
+++ b/signer/core/signed_data_test.go
@@ -0,0 +1,774 @@
+// 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"
+ "fmt"
+ "math/big"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/accounts/keystore"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/common/hexutil"
+)
+
+var typesStandard = 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",
+ },
+ },
+}
+
+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{
+ "Ether Mail",
+ "1",
+ big.NewInt(1),
+ "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
+ "",
+}
+
+var messageStandard = map[string]interface{}{
+ "from": map[string]interface{}{
+ "name": "Cow",
+ "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826",
+ },
+ "to": map[string]interface{}{
+ "name": "Bob",
+ "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB",
+ },
+ "contents": "Hello, Bob!",
+}
+
+var typedData = TypedData{
+ Types: typesStandard,
+ PrimaryType: primaryType,
+ Domain: domainStandard,
+ Message: messageStandard,
+}
+
+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])
+
+ control <- "Y"
+ control <- "wrongpassword"
+ signature, err := api.SignData(context.Background(), TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world")))
+ if signature != nil {
+ t.Errorf("Expected nil-data, got %x", signature)
+ }
+ if err != keystore.ErrDecrypt {
+ t.Errorf("Expected ErrLocked! '%v'", err)
+ }
+ control <- "No way"
+ signature, err = api.SignData(context.Background(), TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world")))
+ if signature != nil {
+ t.Errorf("Expected nil-data, got %x", signature)
+ }
+ if err != ErrRequestDenied {
+ t.Errorf("Expected ErrRequestDenied! '%v'", err)
+ }
+ // text/plain
+ control <- "Y"
+ control <- "a_long_password"
+ signature, err = api.SignData(context.Background(), TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world")))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if signature == nil || len(signature) != 65 {
+ t.Errorf("Expected 65 byte signature (got %d bytes)", len(signature))
+ }
+ // data/typed
+ control <- "Y"
+ control <- "a_long_password"
+ signature, err = api.SignTypedData(context.Background(), a, typedData)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if signature == nil || len(signature) != 65 {
+ t.Errorf("Expected 65 byte signature (got %d bytes)", len(signature))
+ }
+}
+
+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))
+ if mainHash != "0xc52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e" {
+ t.Errorf("Expected different hashStruct result (got %s)", mainHash)
+ }
+
+ hash, err = typedData.HashStruct("EIP712Domain", typedData.Domain.Map())
+ if err != nil {
+ t.Error(err)
+ }
+ domainHash := fmt.Sprintf("0x%s", common.Bytes2Hex(hash))
+ if domainHash != "0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f" {
+ t.Errorf("Expected different domain hashStruct result (got %s)", 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)
+ }
+
+ 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)
+ }
+}
+
+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)
+ }
+}
+
+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))
+ if dataEncoding != "0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8" {
+ t.Errorf("Expected different encodeData result (got %s)", dataEncoding)
+ }
+}
+
+func TestMalformedDomainkeys(t *testing.T) {
+ // Verifies that malformed domain keys are properly caught:
+ //{
+ // "name": "Ether Mail",
+ // "version": "1",
+ // "chainId": 1,
+ // "vxerifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
+ //}
+ 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 malformedDomainTypedData TypedData
+ err := json.Unmarshal([]byte(jsonTypedData), &malformedDomainTypedData)
+ if err != nil {
+ t.Fatalf("unmarshalling failed '%v'", err)
+ }
+ _, err = malformedDomainTypedData.HashStruct("EIP712Domain", malformedDomainTypedData.Domain.Map())
+ if err == nil || err.Error() != "provided data '<nil>' doesn't match type 'address'" {
+ t.Errorf("Expected `provided data '<nil>' doesn't match type 'address'`, got '%v'", err)
+ }
+}
+
+func TestMalformedTypesAndExtradata(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)
+ }
+
+ 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 TestTypeMismatch(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 mismatchTypedData TypedData
+ err := json.Unmarshal([]byte(jsonTypedData), &mismatchTypedData)
+ if err != nil {
+ t.Fatalf("unmarshalling failed '%v'", err)
+ }
+ _, err = mismatchTypedData.HashStruct(mismatchTypedData.PrimaryType, mismatchTypedData.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)
+ }
+
+ mismatchTypedData.Types["Mail"][2].Type = "Blahonga"
+ _, err = mismatchTypedData.HashStruct(mismatchTypedData.PrimaryType, mismatchTypedData.Message)
+ if err == nil || err.Error() != "reference type 'Blahonga' is undefined" {
+ t.Fatalf("Expected `reference type 'Blahonga' is undefined`, got '%v'", err)
+ }
+}
+
+func TestTypeOverflow(t *testing.T) {
+ // Verifies data that doesn't fit into it:
+ //{
+ // "test": 65536 <-- test defined as uint8
+ //}
+ var overflowTypedData TypedData
+ err := json.Unmarshal([]byte(jsonTypedData), &overflowTypedData)
+ if err != nil {
+ t.Fatalf("unmarshalling failed '%v'", err)
+ }
+ // Set test to something outside uint8
+ (overflowTypedData.Message["from"]).(map[string]interface{})["test"] = big.NewInt(65536)
+
+ _, err = overflowTypedData.HashStruct(overflowTypedData.PrimaryType, overflowTypedData.Message)
+ if err == nil || err.Error() != "integer larger than 'uint8'" {
+ t.Fatalf("Expected `integer larger than 'uint8'`, got '%v'", err)
+ }
+
+ (overflowTypedData.Message["from"]).(map[string]interface{})["test"] = big.NewInt(3)
+ (overflowTypedData.Message["to"]).(map[string]interface{})["test"] = big.NewInt(4)
+
+ _, err = overflowTypedData.HashStruct(overflowTypedData.PrimaryType, overflowTypedData.Message)
+ if err != nil {
+ t.Fatalf("Expected no err, got '%v'", err)
+ }
+}
+
+func TestArray(t *testing.T) {
+ // Makes sure that arrays work fine
+ //{
+ // "type": "address[]"
+ //},
+ //{
+ // "type": "string[]"
+ //},
+ //{
+ // "type": "uint16[]",
+ //}
+
+ jsonTypedData := `
+ {
+ "types": {
+ "EIP712Domain": [
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "version",
+ "type": "string"
+ },
+ {
+ "name": "chainId",
+ "type": "uint256"
+ },
+ {
+ "name": "verifyingContract",
+ "type": "address"
+ }
+ ],
+ "Foo": [
+ {
+ "name": "bar",
+ "type": "address[]"
+ }
+ ]
+ },
+ "primaryType": "Foo",
+ "domain": {
+ "name": "Lorem",
+ "version": "1",
+ "chainId": 1,
+ "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
+ },
+ "message": {
+ "bar": [
+ "0x0000000000000000000000000000000000000001",
+ "0x0000000000000000000000000000000000000002",
+ "0x0000000000000000000000000000000000000003"
+ ]
+ }
+ }
+ `
+ var arrayTypedData TypedData
+ err := json.Unmarshal([]byte(jsonTypedData), &arrayTypedData)
+ if err != nil {
+ t.Fatalf("unmarshalling failed '%v'", err)
+ }
+ _, err = arrayTypedData.HashStruct(arrayTypedData.PrimaryType, arrayTypedData.Message)
+ if err != nil {
+ t.Fatalf("Expected no err, got '%v'", err)
+ }
+
+ // Change array to string
+ arrayTypedData.Types["Foo"][0].Type = "string[]"
+ arrayTypedData.Message["bar"] = []interface{}{
+ "lorem",
+ "ipsum",
+ "dolores",
+ }
+ _, err = arrayTypedData.HashStruct(arrayTypedData.PrimaryType, arrayTypedData.Message)
+ if err != nil {
+ t.Fatalf("Expected no err, got '%v'", err)
+ }
+
+ // Change array to uint
+ arrayTypedData.Types["Foo"][0].Type = "uint[]"
+ arrayTypedData.Message["bar"] = []interface{}{
+ big.NewInt(1955),
+ big.NewInt(108),
+ big.NewInt(44010),
+ }
+ _, err = arrayTypedData.HashStruct(arrayTypedData.PrimaryType, arrayTypedData.Message)
+ if err != nil {
+ t.Fatalf("Expected no err, got '%v'", err)
+ }
+
+ // Should not work with fixed-size arrays
+ arrayTypedData.Types["Foo"][0].Type = "uint[3]"
+ _, err = arrayTypedData.HashStruct(arrayTypedData.PrimaryType, arrayTypedData.Message)
+ if err == nil || err.Error() != "unknown type 'uint[3]'" {
+ t.Fatalf("Expected `unknown type 'uint[3]'`, got '%v'", err)
+ }
+}
+
+func TestCustomTypeAsArray(t *testing.T) {
+ 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"
+ }
+ ],
+ "Person[]": [
+ {
+ "name": "baz",
+ "type": "string"
+ }
+ ],
+ "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": {"baz": "foo"},
+ "contents": "Hello, Bob!"
+ }
+ }
+
+`
+ var malformedTypedData TypedData
+ err := json.Unmarshal([]byte(jsonTypedData), &malformedTypedData)
+ if err != nil {
+ t.Fatalf("unmarshalling failed '%v'", err)
+ }
+ _, err = malformedTypedData.HashStruct("EIP712Domain", malformedTypedData.Domain.Map())
+ if err != nil {
+ t.Errorf("Expected no error, got '%v'", err)
+ }
+}
+
+func TestFormatter(t *testing.T) {
+
+ var d TypedData
+ err := json.Unmarshal([]byte(jsonTypedData), &d)
+ if err != nil {
+ t.Fatalf("unmarshalling failed '%v'", err)
+ }
+ formatted := d.Format()
+ for _, item := range formatted {
+ fmt.Printf("'%v'\n", item.Pprint(0))
+ }
+
+ j, _ := json.Marshal(formatted)
+ fmt.Printf("'%v'\n", string(j))
+}
diff --git a/signer/core/types.go b/signer/core/types.go
index 128055774..8acfa7a6a 100644
--- a/signer/core/types.go
+++ b/signer/core/types.go
@@ -19,9 +19,8 @@ package core
import (
"encoding/json"
"fmt"
- "strings"
-
"math/big"
+ "strings"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"