diff options
author | Paul Berg <hello@paulrberg.com> | 2019-02-06 15:30:49 +0800 |
---|---|---|
committer | Martin Holst Swende <martin@swende.se> | 2019-02-06 15:30:49 +0800 |
commit | 572baae10a28da2d02085df7e2f3a282883f5d6e (patch) | |
tree | 3e5424ac64dbeae440604d32aed4293199179c99 /signer/core | |
parent | 7c60d0a6a2d3925c2862cbbb188988475619fd0d (diff) | |
download | go-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.go | 7 | ||||
-rw-r--r-- | signer/core/abihelper_test.go | 5 | ||||
-rw-r--r-- | signer/core/api.go | 74 | ||||
-rw-r--r-- | signer/core/api_test.go | 39 | ||||
-rw-r--r-- | signer/core/auditlog.go | 27 | ||||
-rw-r--r-- | signer/core/cliui.go | 9 | ||||
-rw-r--r-- | signer/core/signed_data.go | 899 | ||||
-rw-r--r-- | signer/core/signed_data_test.go | 774 | ||||
-rw-r--r-- | signer/core/types.go | 3 |
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" |