diff --git a/accounts/accounts.go b/accounts/accounts.go
index 11232b19a..b57f282b3 100644
--- a/accounts/accounts.go
+++ b/accounts/accounts.go
@@ -35,6 +35,13 @@ type Account struct {
URL URL `json:"url"` // Optional resource locator within a backend
}
+const (
+ MimetypeTextWithValidator = "text/validator"
+ MimetypeTypedData = "data/typed"
+ MimetypeClique = "application/x-clique-header"
+ MimetypeTextPlain = "text/plain"
+)
+
// Wallet represents a software or hardware wallet that might contain one or more
// accounts (derived from the same seed).
type Wallet interface {
@@ -101,6 +108,12 @@ type Wallet interface {
// the account in a keystore).
SignData(account Account, mimeType string, data []byte) ([]byte, error)
+ // SignDataWithPassphrase is identical to SignData, but also takes a password
+ // NOTE: there's an chance that an erroneous call might mistake the two strings, and
+ // supply password in the mimetype field, or vice versa. Thus, an implementation
+ // should never echo the mimetype or return the mimetype in the error-response
+ SignDataWithPassphrase(account Account, passphrase, mimeType string, data []byte) ([]byte, error)
+
// Signtext requests the wallet to sign the hash of a given piece of data, prefixed
// by the Ethereum prefix scheme
// It looks up the account specified either solely via its address contained within,
@@ -114,6 +127,9 @@ type Wallet interface {
// the account in a keystore).
SignText(account Account, text []byte) ([]byte, error)
+ // SignTextWithPassphrase is identical to Signtext, but also takes a password
+ SignTextWithPassphrase(account Account, passphrase string, hash []byte) ([]byte, error)
+
// SignTx requests the wallet to sign the given transaction.
//
// It looks up the account specified either solely via its address contained within,
@@ -127,18 +143,7 @@ type Wallet interface {
// the account in a keystore).
SignTx(account Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error)
- // SignTextWithPassphrase requests the wallet to sign the given text with the
- // given passphrase as extra authentication information.
- //
- // It looks up the account specified either solely via its address contained within,
- // or optionally with the aid of any location metadata from the embedded URL field.
- SignTextWithPassphrase(account Account, passphrase string, hash []byte) ([]byte, error)
-
- // SignTxWithPassphrase requests the wallet to sign the given transaction, with the
- // given passphrase as extra authentication information.
- //
- // It looks up the account specified either solely via its address contained within,
- // or optionally with the aid of any location metadata from the embedded URL field.
+ // SignTxWithPassphrase is identical to SignTx, but also takes a password
SignTxWithPassphrase(account Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error)
}
@@ -170,9 +175,22 @@ type Backend interface {
//
// This gives context to the signed message and prevents signing of transactions.
func TextHash(data []byte) []byte {
- hash := sha3.NewLegacyKeccak256()
- fmt.Fprintf(hash, "\x19Ethereum Signed Message:\n%d%s", len(data), data)
- return hash.Sum(nil)
+ hash, _ := TextAndHash(data)
+ return hash
+}
+
+// TextAndHash 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 calulcated as
+// keccak256("\x19Ethereum Signed Message:\n"${message length}${message}).
+//
+// This gives context to the signed message and prevents signing of transactions.
+func TextAndHash(data []byte) ([]byte, string) {
+ msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), string(data))
+ hasher := sha3.NewLegacyKeccak256()
+ hasher.Write([]byte(msg))
+ return hasher.Sum(nil), msg
}
// WalletEventType represents the different event types that can be fired by
diff --git a/accounts/external/backend.go b/accounts/external/backend.go
index 35b9c276d..3b8d50f1b 100644
--- a/accounts/external/backend.go
+++ b/accounts/external/backend.go
@@ -184,11 +184,14 @@ func (api *ExternalSigner) SignTx(account accounts.Account, tx *types.Transactio
}
func (api *ExternalSigner) SignTextWithPassphrase(account accounts.Account, passphrase string, text []byte) ([]byte, error) {
- return []byte{}, fmt.Errorf("operation not supported on external signers")
+ return []byte{}, fmt.Errorf("passphrase-operations not supported on external signers")
}
func (api *ExternalSigner) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
- return nil, fmt.Errorf("operation not supported on external signers")
+ return nil, fmt.Errorf("passphrase-operations not supported on external signers")
+}
+func (api *ExternalSigner) SignDataWithPassphrase(account accounts.Account, passphrase, mimeType string, data []byte) ([]byte, error) {
+ return nil, fmt.Errorf("passphrase-operations not supported on external signers")
}
func (api *ExternalSigner) listAccounts() ([]common.Address, error) {
@@ -201,7 +204,7 @@ func (api *ExternalSigner) listAccounts() ([]common.Address, error) {
func (api *ExternalSigner) signCliqueBlock(a common.Address, rlpBlock hexutil.Bytes) (hexutil.Bytes, error) {
var sig hexutil.Bytes
- if err := api.client.Call(&sig, "account_signData", "application/clique", a, rlpBlock); err != nil {
+ if err := api.client.Call(&sig, "account_signData", core.ApplicationClique.Mime, a, rlpBlock); err != nil {
return nil, err
}
if sig[64] != 27 && sig[64] != 28 {
diff --git a/accounts/keystore/wallet.go b/accounts/keystore/wallet.go
index 0490f39ff..632620ead 100644
--- a/accounts/keystore/wallet.go
+++ b/accounts/keystore/wallet.go
@@ -97,21 +97,18 @@ func (w *keystoreWallet) SignData(account accounts.Account, mimeType string, dat
return w.signHash(account, crypto.Keccak256(data))
}
-func (w *keystoreWallet) SignText(account accounts.Account, text []byte) ([]byte, error) {
- return w.signHash(account, accounts.TextHash(text))
-}
-
-// SignTx implements accounts.Wallet, attempting to sign the given transaction
-// with the given account. If the wallet does not wrap this particular account,
-// an error is returned to avoid account leakage (even though in theory we may
-// be able to sign via our shared keystore backend).
-func (w *keystoreWallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
+// SignDataWithPassphrase signs keccak256(data). The mimetype parameter describes the type of data being signed
+func (w *keystoreWallet) SignDataWithPassphrase(account accounts.Account, passphrase, mimeType string, data []byte) ([]byte, error) {
// Make sure the requested account is contained within
if !w.Contains(account) {
return nil, accounts.ErrUnknownAccount
}
// Account seems valid, request the keystore to sign
- return w.keystore.SignTx(account, tx, chainID)
+ return w.keystore.SignHashWithPassphrase(account, passphrase, crypto.Keccak256(data))
+}
+
+func (w *keystoreWallet) SignText(account accounts.Account, text []byte) ([]byte, error) {
+ return w.signHash(account, accounts.TextHash(text))
}
// SignHashWithPassphrase implements accounts.Wallet, attempting to sign the
@@ -125,6 +122,19 @@ func (w *keystoreWallet) SignTextWithPassphrase(account accounts.Account, passph
return w.keystore.SignHashWithPassphrase(account, passphrase, accounts.TextHash(text))
}
+// SignTx implements accounts.Wallet, attempting to sign the given transaction
+// with the given account. If the wallet does not wrap this particular account,
+// an error is returned to avoid account leakage (even though in theory we may
+// be able to sign via our shared keystore backend).
+func (w *keystoreWallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
+ // Make sure the requested account is contained within
+ if !w.Contains(account) {
+ return nil, accounts.ErrUnknownAccount
+ }
+ // Account seems valid, request the keystore to sign
+ return w.keystore.SignTx(account, tx, chainID)
+}
+
// SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given
// transaction with the given account using passphrase as extra authentication.
func (w *keystoreWallet) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {
diff --git a/accounts/usbwallet/wallet.go b/accounts/usbwallet/wallet.go
index a99dcd0f5..feab505c9 100644
--- a/accounts/usbwallet/wallet.go
+++ b/accounts/usbwallet/wallet.go
@@ -507,6 +507,13 @@ func (w *wallet) SignData(account accounts.Account, mimeType string, data []byte
return w.signHash(account, crypto.Keccak256(data))
}
+// SignDataWithPassphrase implements accounts.Wallet, attempting to sign the given
+// data with the given account using passphrase as extra authentication.
+// Since USB wallets don't rely on passphrases, these are silently ignored.
+func (w *wallet) SignDataWithPassphrase(account accounts.Account, passphrase, mimeType string, data []byte) ([]byte, error) {
+ return w.SignData(account, mimeType, data)
+}
+
func (w *wallet) SignText(account accounts.Account, text []byte) ([]byte, error) {
return w.signHash(account, accounts.TextHash(text))
}
diff --git a/cmd/clef/README.md b/cmd/clef/README.md
index c9461be10..2e1dac299 100644
--- a/cmd/clef/README.md
+++ b/cmd/clef/README.md
@@ -189,7 +189,9 @@ None
"method": "account_new",
"params": []
}
-
+```
+Response
+```
{
"id": 0,
"jsonrpc": "2.0",
@@ -222,7 +224,9 @@ None
"jsonrpc": "2.0",
"method": "account_list"
}
-
+```
+Response
+```
{
"id": 1,
"jsonrpc": "2.0",
@@ -285,8 +289,8 @@ Response
```json
{
+ "id": 2,
"jsonrpc": "2.0",
- "id": 67,
"error": {
"code": -32000,
"message": "Request denied"
@@ -298,6 +302,7 @@ Response
```json
{
+ "id": 67,
"jsonrpc": "2.0",
"method": "account_signTransaction",
"params": [
@@ -311,8 +316,7 @@ Response
"data": "0x4401a6e40000000000000000000000000000000000000000000000000000000000000012"
},
"safeSend(address)"
- ],
- "id": 67
+ ]
}
```
Response
@@ -346,15 +350,18 @@ Bash example:
{"jsonrpc":"2.0","id":67,"result":{"raw":"0xf88380018203339407a565b7ed7d7a678680a4c162885bedbb695fe080a44401a6e4000000000000000000000000000000000000000000000000000000000000001226a0223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20ea02aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663","tx":{"nonce":"0x0","gasPrice":"0x1","gas":"0x333","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0","value":"0x0","input":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012","v":"0x26","r":"0x223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20e","s":"0x2aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663","hash":"0xeba2df809e7a612a0a0d444ccfa5c839624bdc00dd29e3340d46df3870f8a30e"}}}
```
-
-### account_sign
+### account_signData
#### Sign data
Signs a chunk of data and returns the calculated signature.
#### Arguments
+ - content type [string]: type of signed data
+ - `text/validator`: hex data with custom validator defined in a contract
+ - `application/clique`: [clique](https://github.com/ethereum/EIPs/issues/225) headers
+ - `text/plain`: simple hex data validated by `account_ecRecover`
- account [address]: account to sign with
- - data [data]: data to sign
+ - data [object]: data to sign
#### Result
- calculated signature [data]
@@ -364,8 +371,9 @@ Bash example:
{
"id": 3,
"jsonrpc": "2.0",
- "method": "account_sign",
+ "method": "account_signData",
"params": [
+ "data/plain",
"0x1923f626bb8dc025849e00f99c25fe2b2f7fb0db",
"0xaabbccdd"
]
@@ -381,11 +389,109 @@ Response
}
```
+### account_signTypedData
+
+#### Sign data
+ Signs a chunk of structured data conformant to [EIP712]([EIP-712](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md)) and returns the calculated signature.
+
+#### Arguments
+ - account [address]: account to sign with
+ - data [object]: data to sign
+
+#### Result
+ - calculated signature [data]
+
+#### Sample call
+```json
+{
+ "id": 68,
+ "jsonrpc": "2.0",
+ "method": "account_signTypedData",
+ "params": [
+ "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826",
+ {
+ "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,
+ "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
+ },
+ "message": {
+ "from": {
+ "name": "Cow",
+ "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
+ },
+ "to": {
+ "name": "Bob",
+ "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
+ },
+ "contents": "Hello, Bob!"
+ }
+ }
+ ]
+}
+```
+Response
+
+```json
+{
+ "id": 1,
+ "jsonrpc": "2.0",
+ "result": "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c"
+}
+```
+
### account_ecRecover
-#### Recover address
- Derive the address from the account that was used to sign data from the data and signature.
-
+#### Sign data
+
+Derive the address from the account that was used to sign data with content type `text/plain` and the signature.
+
#### Arguments
- data [data]: data that was signed
- signature [data]: the signature to verify
@@ -400,6 +506,7 @@ Response
"jsonrpc": "2.0",
"method": "account_ecRecover",
"params": [
+ "data/plain",
"0xaabbccdd",
"0x5b6693f153b48ec1c706ba4169960386dbaa6903e249cc79a8e6ddc434451d417e1e57327872c7f538beeb323c300afa9999a3d4a5de6caf3be0d5ef832b67ef1c"
]
@@ -413,7 +520,6 @@ Response
"jsonrpc": "2.0",
"result": "0x1923f626bb8dc025849e00f99c25fe2b2f7fb0db"
}
-
```
### account_import
@@ -458,7 +564,7 @@ Response
},
"id": "09bccb61-b8d3-4e93-bf4f-205a8194f0b9",
"version": 3
- },
+ }
]
}
```
diff --git a/cmd/clef/extapi_changelog.md b/cmd/clef/extapi_changelog.md
index 6c2c3e819..25f819bdd 100644
--- a/cmd/clef/extapi_changelog.md
+++ b/cmd/clef/extapi_changelog.md
@@ -1,5 +1,15 @@
### Changelog for external API
+#### 5.0.0
+
+* The external `account_EcRecover`-method was reimplemented.
+* The external method `account_sign(address, data)` was replaced with `account_signData(contentType, address, data)`.
+The addition of `contentType` makes it possible to use the method for different types of objects, such as:
+ * signing data with an intended validator (not yet implemented)
+ * signing clique headers,
+ * signing plain personal messages,
+* The external method `account_signTypedData` implements [EIP-712](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md) and makes it possible to sign typed data.
+
#### 4.0.0
* The external `account_Ecrecover`-method was removed.
diff --git a/cmd/clef/intapi_changelog.md b/cmd/clef/intapi_changelog.md
index 92a39a268..6388af730 100644
--- a/cmd/clef/intapi_changelog.md
+++ b/cmd/clef/intapi_changelog.md
@@ -1,5 +1,9 @@
### Changelog for internal API (ui-api)
+### 3.1.0
+
+* Add `ContentType string` to `SignDataRequest` to accommodate the latest EIP-191 and EIP-712 implementations.
+
### 3.0.0
* Make use of `OnInputRequired(info UserInputRequest)` for obtaining master password during startup
diff --git a/cmd/clef/main.go b/cmd/clef/main.go
index e2b85288d..38821102d 100644
--- a/cmd/clef/main.go
+++ b/cmd/clef/main.go
@@ -28,6 +28,7 @@ import (
"fmt"
"io"
"io/ioutil"
+ "math/big"
"os"
"os/signal"
"os/user"
@@ -39,9 +40,11 @@ import (
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/console"
+ "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
+ "github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum/go-ethereum/signer/core"
"github.com/ethereum/go-ethereum/signer/rules"
@@ -623,10 +626,40 @@ func testExternalUI(api *core.SignerAPI) {
}
var err error
+ cliqueHeader := types.Header{
+ common.HexToHash("0000H45H"),
+ common.HexToHash("0000H45H"),
+ common.HexToAddress("0000H45H"),
+ common.HexToHash("0000H00H"),
+ common.HexToHash("0000H45H"),
+ common.HexToHash("0000H45H"),
+ types.Bloom{},
+ big.NewInt(1337),
+ big.NewInt(1337),
+ 1338,
+ 1338,
+ big.NewInt(1338),
+ []byte("Extra data Extra data Extra data Extra data Extra data Extra data Extra data Extra data"),
+ common.HexToHash("0x0000H45H"),
+ types.BlockNonce{},
+ }
+ cliqueRlp, err := rlp.EncodeToBytes(cliqueHeader)
+ if err != nil {
+ utils.Fatalf("Should not error: %v", err)
+ }
+ addr, err := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899")
+ if err != nil {
+ utils.Fatalf("Should not error: %v", err)
+ }
+ _, err = api.SignData(ctx, "application/clique", *addr, cliqueRlp)
+ checkErr("SignData", err)
+
_, err = api.SignTransaction(ctx, core.SendTxArgs{From: common.MixedcaseAddress{}}, nil)
checkErr("SignTransaction", err)
- _, err = api.Sign(ctx, common.MixedcaseAddress{}, common.Hex2Bytes("01020304"))
- checkErr("Sign", err)
+ _, err = api.SignData(ctx, "text/plain", common.MixedcaseAddress{}, common.Hex2Bytes("01020304"))
+ checkErr("SignData", err)
+ //_, err = api.SignTypedData(ctx, common.MixedcaseAddress{}, core.TypedData{})
+ //checkErr("SignTypedData", err)
_, err = api.List(ctx)
checkErr("List", err)
_, err = api.New(ctx)
@@ -646,7 +679,6 @@ func testExternalUI(api *core.SignerAPI) {
} else {
log.Info("No errors")
}
-
}
// getPassPhrase retrieves the password associated with clef, either fetched
diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go
index c0f78ce65..967a843de 100644
--- a/consensus/clique/clique.go
+++ b/consensus/clique/clique.go
@@ -616,7 +616,7 @@ func (c *Clique) Seal(chain consensus.ChainReader, block *types.Block, results c
log.Trace("Out-of-turn signing requested", "wiggle", common.PrettyDuration(wiggle))
}
// Sign all the things!
- sighash, err := signFn(accounts.Account{Address: signer}, "application/x-clique-header", CliqueRLP(header))
+ sighash, err := signFn(accounts.Account{Address: signer}, accounts.MimetypeClique, CliqueRLP(header))
if err != nil {
return err
}
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 .
+//
+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 .
+//
+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 '' doesn't match type 'address'" {
+ t.Errorf("Expected `provided data '' 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"
diff --git a/signer/rules/rules_test.go b/signer/rules/rules_test.go
index 0b520a15b..d3b2edd55 100644
--- a/signer/rules/rules_test.go
+++ b/signer/rules/rules_test.go
@@ -624,7 +624,7 @@ func TestSignData(t *testing.T) {
function ApproveSignData(r){
if( r.address.toLowerCase() == "0x694267f14675d7e1b9494fd8d72fefe1755710fa")
{
- if(r.message.indexOf("bazonk") >= 0){
+ if(r.message[0].value.indexOf("bazonk") >= 0){
return "Approve"
}
return "Reject"
@@ -636,18 +636,25 @@ function ApproveSignData(r){
t.Errorf("Couldn't create evaluator %v", err)
return
}
- message := []byte("baz bazonk foo")
- hash, msg := core.SignHash(message)
- raw := hexutil.Bytes(message)
+ message := "baz bazonk foo"
+ hash, rawdata := accounts.TextAndHash([]byte(message))
addr, _ := mixAddr("0x694267f14675d7e1b9494fd8d72fefe1755710fa")
fmt.Printf("address %v %v\n", addr.String(), addr.Original())
+
+ nvt := []*core.NameValueType{
+ {
+ Name: "message",
+ Typ: "text/plain",
+ Value: message,
+ },
+ }
resp, err := r.ApproveSignData(&core.SignDataRequest{
Address: *addr,
- Message: msg,
+ Message: nvt,
Hash: hash,
Meta: core.Metadata{Remote: "remoteip", Local: "localip", Scheme: "inproc"},
- Rawdata: raw,
+ Rawdata: []byte(rawdata),
})
if err != nil {
t.Fatalf("Unexpected error %v", err)