signer/core: move EIP-712 types to package apitypes (#24029)

Fixes #23972
MariusVanDerWijden-patch-1
Martin Holst Swende 3 years ago committed by GitHub
parent 93f196c4b0
commit 619a3e7085
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      cmd/clef/main.go
  2. 4
      signer/core/api.go
  3. 2
      signer/core/apitypes/signed_data_internal_test.go
  4. 718
      signer/core/apitypes/types.go
  5. 2
      signer/core/auditlog.go
  6. 14
      signer/core/gnosis_safe.go
  7. 752
      signer/core/signed_data.go
  8. 49
      signer/core/signed_data_test.go
  9. 2
      signer/rules/rules_test.go

@ -898,7 +898,7 @@ func testExternalUI(api *core.SignerAPI) {
addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899")
data := `{"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","test":"2"},"contents":"Hello, Bob!"}}` data := `{"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","test":"2"},"contents":"Hello, Bob!"}}`
//_, err := api.SignData(ctx, accounts.MimetypeTypedData, *addr, hexutil.Encode([]byte(data))) //_, err := api.SignData(ctx, accounts.MimetypeTypedData, *addr, hexutil.Encode([]byte(data)))
var typedData core.TypedData var typedData apitypes.TypedData
json.Unmarshal([]byte(data), &typedData) json.Unmarshal([]byte(data), &typedData)
_, err := api.SignTypedData(ctx, *addr, typedData) _, err := api.SignTypedData(ctx, *addr, typedData)
expectApprove("sign 712 typed data", err) expectApprove("sign 712 typed data", err)
@ -1025,7 +1025,7 @@ func GenDoc(ctx *cli.Context) {
"of the work in canonicalizing and making sense of the data, and it's up to the UI to present" + "of the work in canonicalizing and making sense of the data, and it's up to the UI to present" +
"the user with the contents of the `message`" "the user with the contents of the `message`"
sighash, msg := accounts.TextAndHash([]byte("hello world")) sighash, msg := accounts.TextAndHash([]byte("hello world"))
messages := []*core.NameValueType{{Name: "message", Value: msg, Typ: accounts.MimetypeTextPlain}} messages := []*apitypes.NameValueType{{Name: "message", Value: msg, Typ: accounts.MimetypeTextPlain}}
add("SignDataRequest", desc, &core.SignDataRequest{ add("SignDataRequest", desc, &core.SignDataRequest{
Address: common.NewMixedcaseAddress(a), Address: common.NewMixedcaseAddress(a),

@ -57,7 +57,7 @@ type ExternalAPI interface {
// SignData - request to sign the given data (plus prefix) // SignData - request to sign the given data (plus prefix)
SignData(ctx context.Context, contentType string, addr common.MixedcaseAddress, data interface{}) (hexutil.Bytes, error) 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 - request to sign the given structured data (plus prefix)
SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data TypedData) (hexutil.Bytes, error) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data apitypes.TypedData) (hexutil.Bytes, error)
// EcRecover - recover public key from given message and signature // EcRecover - recover public key from given message and signature
EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error)
// Version info about the APIs // Version info about the APIs
@ -235,7 +235,7 @@ type (
ContentType string `json:"content_type"` ContentType string `json:"content_type"`
Address common.MixedcaseAddress `json:"address"` Address common.MixedcaseAddress `json:"address"`
Rawdata []byte `json:"raw_data"` Rawdata []byte `json:"raw_data"`
Messages []*NameValueType `json:"messages"` Messages []*apitypes.NameValueType `json:"messages"`
Callinfo []apitypes.ValidationInfo `json:"call_info"` Callinfo []apitypes.ValidationInfo `json:"call_info"`
Hash hexutil.Bytes `json:"hash"` Hash hexutil.Bytes `json:"hash"`
Meta Metadata `json:"meta"` Meta Metadata `json:"meta"`

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Lesser General Public License // You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package core package apitypes
import ( import (
"bytes" "bytes"

@ -17,16 +17,29 @@
package apitypes package apitypes
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"math/big" "math/big"
"reflect"
"regexp"
"sort"
"strconv"
"strings" "strings"
"unicode"
"unicode/utf8"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
) )
var typedDataReferenceTypeRegexp = regexp.MustCompile(`^[A-Z](\w*)(\[\])?$`)
type ValidationInfo struct { type ValidationInfo struct {
Typ string `json:"type"` Typ string `json:"type"`
Message string `json:"message"` Message string `json:"message"`
@ -154,3 +167,708 @@ func (args *SendTxArgs) ToTransaction() *types.Transaction {
} }
return types.NewTx(data) return types.NewTx(data)
} }
type SigFormat struct {
Mime string
ByteVersion byte
}
var (
IntendedValidator = SigFormat{
accounts.MimetypeDataWithValidator,
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
}
// TypedData is a type to encapsulate EIP-712 typed messages
type TypedData struct {
Types Types `json:"types"`
PrimaryType string `json:"primaryType"`
Domain TypedDataDomain `json:"domain"`
Message TypedDataMessage `json:"message"`
}
// Type is the inner type of an EIP-712 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 {
if len(t.Type) == 0 {
return false
}
// Reference types must have a leading uppercase character
r, _ := utf8.DecodeRuneInString(t.Type)
return unicode.IsUpper(r)
}
type Types map[string][]Type
type TypePriority struct {
Type string
Value uint
}
type TypedDataMessage = map[string]interface{}
// TypedDataDomain represents the domain part of an EIP-712 message.
type TypedDataDomain struct {
Name string `json:"name"`
Version string `json:"version"`
ChainId *math.HexOrDecimal256 `json:"chainId"`
VerifyingContract string `json:"verifyingContract"`
Salt string `json:"salt"`
}
// 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{})
if len(deps) > 0 {
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 exp, got := len(typedData.Types[primaryType]), len(data); exp < got {
return nil, fmt.Errorf("there is extra data provided in the message (%d < %d)", exp, got)
}
// 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
}
// Attempt to parse bytes in different formats: byte array, hex string, hexutil.Bytes.
func parseBytes(encType interface{}) ([]byte, bool) {
switch v := encType.(type) {
case []byte:
return v, true
case hexutil.Bytes:
return v, true
case string:
bytes, err := hexutil.Decode(v)
if err != nil {
return nil, false
}
return bytes, true
default:
return nil, false
}
}
func parseInteger(encType string, encValue interface{}) (*big.Int, error) {
var (
length int
signed = strings.HasPrefix(encType, "int")
b *big.Int
)
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
}
switch v := encValue.(type) {
case *math.HexOrDecimal256:
b = (*big.Int)(v)
case string:
var hexIntValue math.HexOrDecimal256
if err := hexIntValue.UnmarshalText([]byte(v)); err != nil {
return nil, err
}
b = (*big.Int)(&hexIntValue)
case float64:
// JSON parses non-strings as float64. Fail if we cannot
// convert it losslessly
if float64(int64(v)) == v {
b = big.NewInt(int64(v))
} else {
return nil, fmt.Errorf("invalid float value %v for type %v", v, encType)
}
}
if b == nil {
return nil, fmt.Errorf("invalid integer value %v/%v for type %v", encValue, reflect.TypeOf(encValue), encType)
}
if b.BitLen() > length {
return nil, fmt.Errorf("integer larger than '%v'", encType)
}
if !signed && b.Sign() == -1 {
return nil, fmt.Errorf("invalid negative value for unsigned type %v", encType)
}
return b, 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 := parseBytes(encValue)
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 := parseBytes(encValue); !ok || len(byteValue) != length {
return nil, dataMismatchError(encType, encValue)
} else {
// Right-pad the bits
dst := make([]byte, 32)
copy(dst, byteValue)
return dst, nil
}
}
if strings.HasPrefix(encType, "int") || strings.HasPrefix(encType, "uint") {
b, err := parseInteger(encType, encValue)
if err != nil {
return nil, err
}
return math.U256Bytes(b), 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)
}
// 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
}
// 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, error) {
domain, err := typedData.formatData("EIP712Domain", typedData.Domain.Map())
if err != nil {
return nil, err
}
ptype, err := typedData.formatData(typedData.PrimaryType, typedData.Message)
if err != nil {
return nil, err
}
var nvts []*NameValueType
nvts = append(nvts, &NameValueType{
Name: "EIP712Domain",
Value: domain,
Typ: "domain",
})
nvts = append(nvts, &NameValueType{
Name: typedData.PrimaryType,
Value: ptype,
Typ: "primary type",
})
return nvts, nil
}
func (typedData *TypedData) formatData(primaryType string, data map[string]interface{}) ([]*NameValueType, error) {
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, err := typedData.formatData(parsedType, mapValue)
if err != nil {
return nil, err
}
item.Value = mapOutput
} else {
primitiveOutput, err := formatPrimitiveValue(field.Type, encValue)
if err != nil {
return nil, err
}
item.Value = primitiveOutput
}
}
} else if typedData.Types[field.Type] != nil {
if mapValue, ok := encValue.(map[string]interface{}); ok {
mapOutput, err := typedData.formatData(field.Type, mapValue)
if err != nil {
return nil, err
}
item.Value = mapOutput
} else {
item.Value = "<nil>"
}
} else {
primitiveOutput, err := formatPrimitiveValue(field.Type, encValue)
if err != nil {
return nil, err
}
item.Value = primitiveOutput
}
output = append(output, item)
}
return output, nil
}
func formatPrimitiveValue(encType string, encValue interface{}) (string, error) {
switch encType {
case "address":
if stringValue, ok := encValue.(string); !ok {
return "", fmt.Errorf("could not format value %v as address", encValue)
} else {
return common.HexToAddress(stringValue).String(), nil
}
case "bool":
if boolValue, ok := encValue.(bool); !ok {
return "", fmt.Errorf("could not format value %v as bool", encValue)
} else {
return fmt.Sprintf("%t", boolValue), nil
}
case "bytes", "string":
return fmt.Sprintf("%s", encValue), nil
}
if strings.HasPrefix(encType, "bytes") {
return fmt.Sprintf("%s", encValue), nil
}
if strings.HasPrefix(encType, "uint") || strings.HasPrefix(encType, "int") {
if b, err := parseInteger(encType, encValue); err != nil {
return "", err
} else {
return fmt.Sprintf("%d (0x%x)", b, b), nil
}
}
return "", fmt.Errorf("unhandled type %v", encType)
}
// Validate checks if the types object is conformant to the specs
func (t Types) validate() error {
for typeKey, typeArr := range t {
if len(typeKey) == 0 {
return fmt.Errorf("empty type key")
}
for i, typeObj := range typeArr {
if len(typeObj.Type) == 0 {
return fmt.Errorf("type %q:%d: empty Type", typeKey, i)
}
if len(typeObj.Name) == 0 {
return fmt.Errorf("type %q:%d: empty Name", typeKey, i)
}
if typeKey == typeObj.Type {
return fmt.Errorf("type %q cannot reference itself", typeObj.Type)
}
if typeObj.isReferenceType() {
if _, exist := t[typeObj.typeName()]; !exist {
return fmt.Errorf("reference type %q is undefined", typeObj.Type)
}
if !typedDataReferenceTypeRegexp.MatchString(typeObj.Type) {
return fmt.Errorf("unknown reference type %q", typeObj.Type)
}
} else if !isPrimitiveTypeValid(typeObj.Type) {
return fmt.Errorf("unknown type %q", 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[]" ||
primitiveType == "bytes32" ||
primitiveType == "bytes32[]" {
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 == nil && 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{}{}
if domain.ChainId != nil {
dataMap["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
}
// 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 {
if nvt.Value != nil {
output.WriteString(fmt.Sprintf("%q\n", nvt.Value))
} else {
output.WriteString("\n")
}
}
return output.String()
}

@ -89,7 +89,7 @@ func (l *AuditLogger) SignGnosisSafeTx(ctx context.Context, addr common.Mixedcas
return res, e return res, e
} }
func (l *AuditLogger) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data TypedData) (hexutil.Bytes, error) { func (l *AuditLogger) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, data apitypes.TypedData) (hexutil.Bytes, error) {
l.log.Info("SignTypedData", "type", "request", "metadata", MetadataFromContext(ctx).String(), l.log.Info("SignTypedData", "type", "request", "metadata", MetadataFromContext(ctx).String(),
"addr", addr.String(), "data", data) "addr", addr.String(), "data", data)
b, e := l.api.SignTypedData(ctx, addr, data) b, e := l.api.SignTypedData(ctx, addr, data)

@ -34,15 +34,15 @@ type GnosisSafeTx struct {
} }
// ToTypedData converts the tx to a EIP-712 Typed Data structure for signing // ToTypedData converts the tx to a EIP-712 Typed Data structure for signing
func (tx *GnosisSafeTx) ToTypedData() TypedData { func (tx *GnosisSafeTx) ToTypedData() apitypes.TypedData {
var data hexutil.Bytes var data hexutil.Bytes
if tx.Data != nil { if tx.Data != nil {
data = *tx.Data data = *tx.Data
} }
gnosisTypedData := TypedData{ gnosisTypedData := apitypes.TypedData{
Types: Types{ Types: apitypes.Types{
"EIP712Domain": []Type{{Name: "verifyingContract", Type: "address"}}, "EIP712Domain": []apitypes.Type{{Name: "verifyingContract", Type: "address"}},
"SafeTx": []Type{ "SafeTx": []apitypes.Type{
{Name: "to", Type: "address"}, {Name: "to", Type: "address"},
{Name: "value", Type: "uint256"}, {Name: "value", Type: "uint256"},
{Name: "data", Type: "bytes"}, {Name: "data", Type: "bytes"},
@ -55,11 +55,11 @@ func (tx *GnosisSafeTx) ToTypedData() TypedData {
{Name: "nonce", Type: "uint256"}, {Name: "nonce", Type: "uint256"},
}, },
}, },
Domain: TypedDataDomain{ Domain: apitypes.TypedDataDomain{
VerifyingContract: tx.Safe.Address().Hex(), VerifyingContract: tx.Safe.Address().Hex(),
}, },
PrimaryType: "SafeTx", PrimaryType: "SafeTx",
Message: TypedDataMessage{ Message: apitypes.TypedDataMessage{
"to": tx.To.Address().Hex(), "to": tx.To.Address().Hex(),
"value": tx.Value.String(), "value": tx.Value.String(),
"data": data, "data": data,

@ -17,24 +17,14 @@
package core package core
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"fmt" "fmt"
"math/big"
"mime" "mime"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"unicode"
"unicode/utf8"
"github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "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/consensus/clique"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
@ -42,88 +32,6 @@ import (
"github.com/ethereum/go-ethereum/signer/core/apitypes" "github.com/ethereum/go-ethereum/signer/core/apitypes"
) )
type SigFormat struct {
Mime string
ByteVersion byte
}
var (
IntendedValidator = SigFormat{
accounts.MimetypeDataWithValidator,
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 {
if len(t.Type) == 0 {
return false
}
// Reference types must have a leading uppercase character
r, _ := utf8.DecodeRuneInString(t.Type)
return unicode.IsUpper(r)
}
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 *math.HexOrDecimal256 `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 // sign receives a request and produces a signature
// //
// Note, the produced signature conforms to the secp256k1 curve R, S and V values, // Note, the produced signature conforms to the secp256k1 curve R, S and V values,
@ -195,14 +103,14 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType
} }
switch mediaType { switch mediaType {
case IntendedValidator.Mime: case apitypes.IntendedValidator.Mime:
// Data with an intended validator // Data with an intended validator
validatorData, err := UnmarshalValidatorData(data) validatorData, err := UnmarshalValidatorData(data)
if err != nil { if err != nil {
return nil, useEthereumV, err return nil, useEthereumV, err
} }
sighash, msg := SignTextValidator(validatorData) sighash, msg := SignTextValidator(validatorData)
messages := []*NameValueType{ messages := []*apitypes.NameValueType{
{ {
Name: "This is a request to sign data intended for a particular validator (see EIP 191 version 0)", Name: "This is a request to sign data intended for a particular validator (see EIP 191 version 0)",
Typ: "description", Typ: "description",
@ -225,11 +133,11 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType
}, },
} }
req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Messages: messages, Hash: sighash} req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Messages: messages, Hash: sighash}
case ApplicationClique.Mime: case apitypes.ApplicationClique.Mime:
// Clique is the Ethereum PoA standard // Clique is the Ethereum PoA standard
stringData, ok := data.(string) stringData, ok := data.(string)
if !ok { if !ok {
return nil, useEthereumV, fmt.Errorf("input for %v must be an hex-encoded string", ApplicationClique.Mime) return nil, useEthereumV, fmt.Errorf("input for %v must be an hex-encoded string", apitypes.ApplicationClique.Mime)
} }
cliqueData, err := hexutil.Decode(stringData) cliqueData, err := hexutil.Decode(stringData)
if err != nil { if err != nil {
@ -251,7 +159,7 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType
if err != nil { if err != nil {
return nil, useEthereumV, err return nil, useEthereumV, err
} }
messages := []*NameValueType{ messages := []*apitypes.NameValueType{
{ {
Name: "Clique header", Name: "Clique header",
Typ: "clique", Typ: "clique",
@ -272,7 +180,7 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType
return nil, useEthereumV, err return nil, useEthereumV, err
} else { } else {
sighash, msg := accounts.TextAndHash(textData) sighash, msg := accounts.TextAndHash(textData)
messages := []*NameValueType{ messages := []*apitypes.NameValueType{
{ {
Name: "message", Name: "message",
Typ: accounts.MimetypeTextPlain, Typ: accounts.MimetypeTextPlain,
@ -291,7 +199,7 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType
// SignTextWithValidator signs the given message which can be further recovered // SignTextWithValidator signs the given message which can be further recovered
// with the given validator. // with the given validator.
// hash = keccak256("\x19\x00"${address}${data}). // hash = keccak256("\x19\x00"${address}${data}).
func SignTextValidator(validatorData ValidatorData) (hexutil.Bytes, string) { func SignTextValidator(validatorData apitypes.ValidatorData) (hexutil.Bytes, string) {
msg := fmt.Sprintf("\x19\x00%s%s", string(validatorData.Address.Bytes()), string(validatorData.Message)) msg := fmt.Sprintf("\x19\x00%s%s", string(validatorData.Address.Bytes()), string(validatorData.Message))
return crypto.Keccak256([]byte(msg)), msg return crypto.Keccak256([]byte(msg)), msg
} }
@ -318,7 +226,7 @@ func cliqueHeaderHashAndRlp(header *types.Header) (hash, rlp []byte, err error)
// It returns // It returns
// - the signature, // - the signature,
// - and/or any error // - and/or any error
func (api *SignerAPI) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, typedData TypedData) (hexutil.Bytes, error) { func (api *SignerAPI) SignTypedData(ctx context.Context, addr common.MixedcaseAddress, typedData apitypes.TypedData) (hexutil.Bytes, error) {
signature, _, err := api.signTypedData(ctx, addr, typedData, nil) signature, _, err := api.signTypedData(ctx, addr, typedData, nil)
return signature, err return signature, err
} }
@ -326,7 +234,7 @@ func (api *SignerAPI) SignTypedData(ctx context.Context, addr common.MixedcaseAd
// signTypedData is identical to the capitalized version, except that it also returns the hash (preimage) // signTypedData is identical to the capitalized version, except that it also returns the hash (preimage)
// - the signature preimage (hash) // - the signature preimage (hash)
func (api *SignerAPI) signTypedData(ctx context.Context, addr common.MixedcaseAddress, func (api *SignerAPI) signTypedData(ctx context.Context, addr common.MixedcaseAddress,
typedData TypedData, validationMessages *apitypes.ValidationMessages) (hexutil.Bytes, hexutil.Bytes, error) { typedData apitypes.TypedData, validationMessages *apitypes.ValidationMessages) (hexutil.Bytes, hexutil.Bytes, error) {
domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map())
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -342,7 +250,7 @@ func (api *SignerAPI) signTypedData(ctx context.Context, addr common.MixedcaseAd
return nil, nil, err return nil, nil, err
} }
req := &SignDataRequest{ req := &SignDataRequest{
ContentType: DataTyped.Mime, ContentType: apitypes.DataTyped.Mime,
Rawdata: rawData, Rawdata: rawData,
Messages: messages, Messages: messages,
Hash: sighash, Hash: sighash,
@ -358,289 +266,6 @@ func (api *SignerAPI) signTypedData(ctx context.Context, addr common.MixedcaseAd
return signature, sighash, nil return signature, sighash, 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{})
if len(deps) > 0 {
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 exp, got := len(typedData.Types[primaryType]), len(data); exp < got {
return nil, fmt.Errorf("there is extra data provided in the message (%d < %d)", exp, got)
}
// 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
}
// Attempt to parse bytes in different formats: byte array, hex string, hexutil.Bytes.
func parseBytes(encType interface{}) ([]byte, bool) {
switch v := encType.(type) {
case []byte:
return v, true
case hexutil.Bytes:
return v, true
case string:
bytes, err := hexutil.Decode(v)
if err != nil {
return nil, false
}
return bytes, true
default:
return nil, false
}
}
func parseInteger(encType string, encValue interface{}) (*big.Int, error) {
var (
length int
signed = strings.HasPrefix(encType, "int")
b *big.Int
)
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
}
switch v := encValue.(type) {
case *math.HexOrDecimal256:
b = (*big.Int)(v)
case string:
var hexIntValue math.HexOrDecimal256
if err := hexIntValue.UnmarshalText([]byte(v)); err != nil {
return nil, err
}
b = (*big.Int)(&hexIntValue)
case float64:
// JSON parses non-strings as float64. Fail if we cannot
// convert it losslessly
if float64(int64(v)) == v {
b = big.NewInt(int64(v))
} else {
return nil, fmt.Errorf("invalid float value %v for type %v", v, encType)
}
}
if b == nil {
return nil, fmt.Errorf("invalid integer value %v/%v for type %v", encValue, reflect.TypeOf(encValue), encType)
}
if b.BitLen() > length {
return nil, fmt.Errorf("integer larger than '%v'", encType)
}
if !signed && b.Sign() == -1 {
return nil, fmt.Errorf("invalid negative value for unsigned type %v", encType)
}
return b, 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 := parseBytes(encValue)
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 := parseBytes(encValue); !ok || len(byteValue) != length {
return nil, dataMismatchError(encType, encValue)
} else {
// Right-pad the bits
dst := make([]byte, 32)
copy(dst, byteValue)
return dst, nil
}
}
if strings.HasPrefix(encType, "int") || strings.HasPrefix(encType, "uint") {
b, err := parseInteger(encType, encValue)
if err != nil {
return nil, err
}
return math.U256Bytes(b), 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. // EcRecover recovers the address associated with the given sig.
// Only compatible with `text/plain` // Only compatible with `text/plain`
func (api *SignerAPI) EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) { func (api *SignerAPI) EcRecover(ctx context.Context, data hexutil.Bytes, sig hexutil.Bytes) (common.Address, error) {
@ -671,376 +296,37 @@ func (api *SignerAPI) EcRecover(ctx context.Context, data hexutil.Bytes, sig hex
} }
// UnmarshalValidatorData converts the bytes input to typed data // UnmarshalValidatorData converts the bytes input to typed data
func UnmarshalValidatorData(data interface{}) (ValidatorData, error) { func UnmarshalValidatorData(data interface{}) (apitypes.ValidatorData, error) {
raw, ok := data.(map[string]interface{}) raw, ok := data.(map[string]interface{})
if !ok { if !ok {
return ValidatorData{}, errors.New("validator input is not a map[string]interface{}") return apitypes.ValidatorData{}, errors.New("validator input is not a map[string]interface{}")
} }
addr, ok := raw["address"].(string) addr, ok := raw["address"].(string)
if !ok { if !ok {
return ValidatorData{}, errors.New("validator address is not sent as a string") return apitypes.ValidatorData{}, errors.New("validator address is not sent as a string")
} }
addrBytes, err := hexutil.Decode(addr) addrBytes, err := hexutil.Decode(addr)
if err != nil { if err != nil {
return ValidatorData{}, err return apitypes.ValidatorData{}, err
} }
if !ok || len(addrBytes) == 0 { if !ok || len(addrBytes) == 0 {
return ValidatorData{}, errors.New("validator address is undefined") return apitypes.ValidatorData{}, errors.New("validator address is undefined")
} }
message, ok := raw["message"].(string) message, ok := raw["message"].(string)
if !ok { if !ok {
return ValidatorData{}, errors.New("message is not sent as a string") return apitypes.ValidatorData{}, errors.New("message is not sent as a string")
} }
messageBytes, err := hexutil.Decode(message) messageBytes, err := hexutil.Decode(message)
if err != nil { if err != nil {
return ValidatorData{}, err return apitypes.ValidatorData{}, err
} }
if !ok || len(messageBytes) == 0 { if !ok || len(messageBytes) == 0 {
return ValidatorData{}, errors.New("message is undefined") return apitypes.ValidatorData{}, errors.New("message is undefined")
} }
return ValidatorData{ return apitypes.ValidatorData{
Address: common.BytesToAddress(addrBytes), Address: common.BytesToAddress(addrBytes),
Message: messageBytes, Message: messageBytes,
}, nil }, 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
}
// 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, error) {
domain, err := typedData.formatData("EIP712Domain", typedData.Domain.Map())
if err != nil {
return nil, err
}
ptype, err := typedData.formatData(typedData.PrimaryType, typedData.Message)
if err != nil {
return nil, err
}
var nvts []*NameValueType
nvts = append(nvts, &NameValueType{
Name: "EIP712Domain",
Value: domain,
Typ: "domain",
})
nvts = append(nvts, &NameValueType{
Name: typedData.PrimaryType,
Value: ptype,
Typ: "primary type",
})
return nvts, nil
}
func (typedData *TypedData) formatData(primaryType string, data map[string]interface{}) ([]*NameValueType, error) {
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, err := typedData.formatData(parsedType, mapValue)
if err != nil {
return nil, err
}
item.Value = mapOutput
} else {
primitiveOutput, err := formatPrimitiveValue(field.Type, encValue)
if err != nil {
return nil, err
}
item.Value = primitiveOutput
}
}
} else if typedData.Types[field.Type] != nil {
if mapValue, ok := encValue.(map[string]interface{}); ok {
mapOutput, err := typedData.formatData(field.Type, mapValue)
if err != nil {
return nil, err
}
item.Value = mapOutput
} else {
item.Value = "<nil>"
}
} else {
primitiveOutput, err := formatPrimitiveValue(field.Type, encValue)
if err != nil {
return nil, err
}
item.Value = primitiveOutput
}
output = append(output, item)
}
return output, nil
}
func formatPrimitiveValue(encType string, encValue interface{}) (string, error) {
switch encType {
case "address":
if stringValue, ok := encValue.(string); !ok {
return "", fmt.Errorf("could not format value %v as address", encValue)
} else {
return common.HexToAddress(stringValue).String(), nil
}
case "bool":
if boolValue, ok := encValue.(bool); !ok {
return "", fmt.Errorf("could not format value %v as bool", encValue)
} else {
return fmt.Sprintf("%t", boolValue), nil
}
case "bytes", "string":
return fmt.Sprintf("%s", encValue), nil
}
if strings.HasPrefix(encType, "bytes") {
return fmt.Sprintf("%s", encValue), nil
}
if strings.HasPrefix(encType, "uint") || strings.HasPrefix(encType, "int") {
if b, err := parseInteger(encType, encValue); err != nil {
return "", err
} else {
return fmt.Sprintf("%d (0x%x)", b, b), nil
}
}
return "", fmt.Errorf("unhandled type %v", encType)
}
// 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 {
if nvt.Value != nil {
output.WriteString(fmt.Sprintf("%q\n", nvt.Value))
} else {
output.WriteString("\n")
}
}
return output.String()
}
// Validate checks if the types object is conformant to the specs
func (t Types) validate() error {
for typeKey, typeArr := range t {
if len(typeKey) == 0 {
return fmt.Errorf("empty type key")
}
for i, typeObj := range typeArr {
if len(typeObj.Type) == 0 {
return fmt.Errorf("type %q:%d: empty Type", typeKey, i)
}
if len(typeObj.Name) == 0 {
return fmt.Errorf("type %q:%d: empty Name", typeKey, i)
}
if typeKey == typeObj.Type {
return fmt.Errorf("type %q cannot reference itself", typeObj.Type)
}
if typeObj.isReferenceType() {
if _, exist := t[typeObj.typeName()]; !exist {
return fmt.Errorf("reference type %q is undefined", typeObj.Type)
}
if !typedDataReferenceTypeRegexp.MatchString(typeObj.Type) {
return fmt.Errorf("unknown reference type %q", typeObj.Type)
}
} else if !isPrimitiveTypeValid(typeObj.Type) {
return fmt.Errorf("unknown type %q", 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[]" ||
primitiveType == "bytes32" ||
primitiveType == "bytes32[]" {
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 == nil && 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{}{}
if domain.ChainId != nil {
dataMap["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
}

@ -32,9 +32,10 @@ import (
"github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/signer/core" "github.com/ethereum/go-ethereum/signer/core"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
) )
var typesStandard = core.Types{ var typesStandard = apitypes.Types{
"EIP712Domain": { "EIP712Domain": {
{ {
Name: "name", Name: "name",
@ -153,12 +154,12 @@ var jsonTypedData = `
const primaryType = "Mail" const primaryType = "Mail"
var domainStandard = core.TypedDataDomain{ var domainStandard = apitypes.TypedDataDomain{
"Ether Mail", Name: "Ether Mail",
"1", Version: "1",
math.NewHexOrDecimal256(1), ChainId: math.NewHexOrDecimal256(1),
"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", VerifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
"", Salt: "",
} }
var messageStandard = map[string]interface{}{ var messageStandard = map[string]interface{}{
@ -173,7 +174,7 @@ var messageStandard = map[string]interface{}{
"contents": "Hello, Bob!", "contents": "Hello, Bob!",
} }
var typedData = core.TypedData{ var typedData = apitypes.TypedData{
Types: typesStandard, Types: typesStandard,
PrimaryType: primaryType, PrimaryType: primaryType,
Domain: domainStandard, Domain: domainStandard,
@ -194,7 +195,7 @@ func TestSignData(t *testing.T) {
control.approveCh <- "Y" control.approveCh <- "Y"
control.inputCh <- "wrongpassword" control.inputCh <- "wrongpassword"
signature, err := api.SignData(context.Background(), core.TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world"))) signature, err := api.SignData(context.Background(), apitypes.TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world")))
if signature != nil { if signature != nil {
t.Errorf("Expected nil-data, got %x", signature) t.Errorf("Expected nil-data, got %x", signature)
} }
@ -202,7 +203,7 @@ func TestSignData(t *testing.T) {
t.Errorf("Expected ErrLocked! '%v'", err) t.Errorf("Expected ErrLocked! '%v'", err)
} }
control.approveCh <- "No way" control.approveCh <- "No way"
signature, err = api.SignData(context.Background(), core.TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world"))) signature, err = api.SignData(context.Background(), apitypes.TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world")))
if signature != nil { if signature != nil {
t.Errorf("Expected nil-data, got %x", signature) t.Errorf("Expected nil-data, got %x", signature)
} }
@ -212,7 +213,7 @@ func TestSignData(t *testing.T) {
// text/plain // text/plain
control.approveCh <- "Y" control.approveCh <- "Y"
control.inputCh <- "a_long_password" control.inputCh <- "a_long_password"
signature, err = api.SignData(context.Background(), core.TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world"))) signature, err = api.SignData(context.Background(), apitypes.TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world")))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -232,13 +233,13 @@ func TestSignData(t *testing.T) {
} }
func TestDomainChainId(t *testing.T) { func TestDomainChainId(t *testing.T) {
withoutChainID := core.TypedData{ withoutChainID := apitypes.TypedData{
Types: core.Types{ Types: apitypes.Types{
"EIP712Domain": []core.Type{ "EIP712Domain": []apitypes.Type{
{Name: "name", Type: "string"}, {Name: "name", Type: "string"},
}, },
}, },
Domain: core.TypedDataDomain{ Domain: apitypes.TypedDataDomain{
Name: "test", Name: "test",
}, },
} }
@ -250,14 +251,14 @@ func TestDomainChainId(t *testing.T) {
if _, err := withoutChainID.HashStruct("EIP712Domain", withoutChainID.Domain.Map()); err != nil { if _, err := withoutChainID.HashStruct("EIP712Domain", withoutChainID.Domain.Map()); err != nil {
t.Errorf("Expected the typedData to encode the domain successfully, got %v", err) t.Errorf("Expected the typedData to encode the domain successfully, got %v", err)
} }
withChainID := core.TypedData{ withChainID := apitypes.TypedData{
Types: core.Types{ Types: apitypes.Types{
"EIP712Domain": []core.Type{ "EIP712Domain": []apitypes.Type{
{Name: "name", Type: "string"}, {Name: "name", Type: "string"},
{Name: "chainId", Type: "uint256"}, {Name: "chainId", Type: "uint256"},
}, },
}, },
Domain: core.TypedDataDomain{ Domain: apitypes.TypedDataDomain{
Name: "test", Name: "test",
ChainId: math.NewHexOrDecimal256(1), ChainId: math.NewHexOrDecimal256(1),
}, },
@ -323,7 +324,7 @@ func TestEncodeData(t *testing.T) {
} }
func TestFormatter(t *testing.T) { func TestFormatter(t *testing.T) {
var d core.TypedData var d apitypes.TypedData
err := json.Unmarshal([]byte(jsonTypedData), &d) err := json.Unmarshal([]byte(jsonTypedData), &d)
if err != nil { if err != nil {
t.Fatalf("unmarshalling failed '%v'", err) t.Fatalf("unmarshalling failed '%v'", err)
@ -337,7 +338,7 @@ func TestFormatter(t *testing.T) {
t.Logf("'%v'\n", string(j)) t.Logf("'%v'\n", string(j))
} }
func sign(typedData core.TypedData) ([]byte, []byte, error) { func sign(typedData apitypes.TypedData) ([]byte, []byte, error) {
domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map())
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -366,7 +367,7 @@ func TestJsonFiles(t *testing.T) {
t.Errorf("Failed to read file %v: %v", fInfo.Name(), err) t.Errorf("Failed to read file %v: %v", fInfo.Name(), err)
continue continue
} }
var typedData core.TypedData var typedData apitypes.TypedData
err = json.Unmarshal(data, &typedData) err = json.Unmarshal(data, &typedData)
if err != nil { if err != nil {
t.Errorf("Test %d, file %v, json unmarshalling failed: %v", i, fInfo.Name(), err) t.Errorf("Test %d, file %v, json unmarshalling failed: %v", i, fInfo.Name(), err)
@ -398,7 +399,7 @@ func TestFuzzerFiles(t *testing.T) {
t.Errorf("Failed to read file %v: %v", fInfo.Name(), err) t.Errorf("Failed to read file %v: %v", fInfo.Name(), err)
continue continue
} }
var typedData core.TypedData var typedData apitypes.TypedData
err = json.Unmarshal(data, &typedData) err = json.Unmarshal(data, &typedData)
if err != nil { if err != nil {
t.Errorf("Test %d, file %v, json unmarshalling failed: %v", i, fInfo.Name(), err) t.Errorf("Test %d, file %v, json unmarshalling failed: %v", i, fInfo.Name(), err)
@ -498,7 +499,7 @@ var gnosisTx = `
// TestGnosisTypedData tests the scenario where a user submits a full EIP-712 // TestGnosisTypedData tests the scenario where a user submits a full EIP-712
// struct without using the gnosis-specific endpoint // struct without using the gnosis-specific endpoint
func TestGnosisTypedData(t *testing.T) { func TestGnosisTypedData(t *testing.T) {
var td core.TypedData var td apitypes.TypedData
err := json.Unmarshal([]byte(gnosisTypedData), &td) err := json.Unmarshal([]byte(gnosisTypedData), &td)
if err != nil { if err != nil {
t.Fatalf("unmarshalling failed '%v'", err) t.Fatalf("unmarshalling failed '%v'", err)

@ -605,7 +605,7 @@ function ApproveSignData(r){
t.Logf("address %v %v\n", addr.String(), addr.Original()) t.Logf("address %v %v\n", addr.String(), addr.Original())
nvt := []*core.NameValueType{ nvt := []*apitypes.NameValueType{
{ {
Name: "message", Name: "message",
Typ: "text/plain", Typ: "text/plain",

Loading…
Cancel
Save