signer: enable typed data signing from signer rpc (#26241)

This PR should makes it easier to sign EIP-712 typed data via the accounts.Wallet API, by using the mimetype for typed data. 

Co-authored-by: nasdf <keenan.nemetz@gmail.com>
This commit is contained in:
Martin Holst Swende 2022-11-25 09:13:45 +01:00 committed by GitHub
parent add1bff13f
commit 6a4e05c93a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 90 additions and 55 deletions

@ -18,6 +18,7 @@ package core
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"mime" "mime"
@ -135,11 +136,7 @@ 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 apitypes.ApplicationClique.Mime: case apitypes.ApplicationClique.Mime:
// Clique is the Ethereum PoA standard // Clique is the Ethereum PoA standard
stringData, ok := data.(string) cliqueData, err := fromHex(data)
if !ok {
return nil, useEthereumV, fmt.Errorf("input for %v must be an hex-encoded string", apitypes.ApplicationClique.Mime)
}
cliqueData, err := hexutil.Decode(stringData)
if err != nil { if err != nil {
return nil, useEthereumV, err return nil, useEthereumV, err
} }
@ -167,27 +164,30 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType
// Clique uses V on the form 0 or 1 // Clique uses V on the form 0 or 1
useEthereumV = false useEthereumV = false
req = &SignDataRequest{ContentType: mediaType, Rawdata: cliqueRlp, Messages: messages, Hash: sighash} req = &SignDataRequest{ContentType: mediaType, Rawdata: cliqueRlp, Messages: messages, Hash: sighash}
case apitypes.DataTyped.Mime:
// EIP-712 conformant typed data
var err error
req, err = typedDataRequest(data)
if err != nil {
return nil, useEthereumV, err
}
default: // also case TextPlain.Mime: default: // also case TextPlain.Mime:
// Calculates an Ethereum ECDSA signature for: // Calculates an Ethereum ECDSA signature for:
// hash = keccak256("\x19Ethereum Signed Message:\n${message length}${message}") // hash = keccak256("\x19Ethereum Signed Message:\n${message length}${message}")
// We expect it to be a string // We expect input to be a hex-encoded string
if stringData, ok := data.(string); !ok { textData, err := fromHex(data)
return nil, useEthereumV, fmt.Errorf("input for text/plain must be an hex-encoded string") if err != nil {
} else { return nil, useEthereumV, err
if textData, err := hexutil.Decode(stringData); err != nil {
return nil, useEthereumV, err
} else {
sighash, msg := accounts.TextAndHash(textData)
messages := []*apitypes.NameValueType{
{
Name: "message",
Typ: accounts.MimetypeTextPlain,
Value: msg,
},
}
req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Messages: messages, Hash: sighash}
}
} }
sighash, msg := accounts.TextAndHash(textData)
messages := []*apitypes.NameValueType{
{
Name: "message",
Typ: accounts.MimetypeTextPlain,
Value: msg,
},
}
req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Messages: messages, Hash: sighash}
} }
req.Address = addr req.Address = addr
req.Meta = MetadataFromContext(ctx) req.Meta = MetadataFromContext(ctx)
@ -233,20 +233,12 @@ func (api *SignerAPI) SignTypedData(ctx context.Context, addr common.MixedcaseAd
// - 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 apitypes.TypedData, validationMessages *apitypes.ValidationMessages) (hexutil.Bytes, hexutil.Bytes, error) { typedData apitypes.TypedData, validationMessages *apitypes.ValidationMessages) (hexutil.Bytes, hexutil.Bytes, error) {
sighash, rawData, err := apitypes.TypedDataAndHash(typedData) req, err := typedDataRequest(typedData)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
messages, err := typedData.Format() req.Address = addr
if err != nil { req.Meta = MetadataFromContext(ctx)
return nil, nil, err
}
req := &SignDataRequest{
ContentType: apitypes.DataTyped.Mime,
Rawdata: []byte(rawData),
Messages: messages,
Hash: sighash,
Address: addr}
if validationMessages != nil { if validationMessages != nil {
req.Callinfo = validationMessages.Messages req.Callinfo = validationMessages.Messages
} }
@ -255,7 +247,46 @@ func (api *SignerAPI) signTypedData(ctx context.Context, addr common.MixedcaseAd
api.UI.ShowError(err.Error()) api.UI.ShowError(err.Error())
return nil, nil, err return nil, nil, err
} }
return signature, sighash, nil return signature, req.Hash, nil
}
// fromHex tries to interpret the data as type string, and convert from
// hexadecimal to []byte
func fromHex(data any) ([]byte, error) {
if stringData, ok := data.(string); ok {
binary, err := hexutil.Decode(stringData)
return binary, err
}
return nil, fmt.Errorf("wrong type %T", data)
}
// typeDataRequest tries to convert the data into a SignDataRequest.
func typedDataRequest(data any) (*SignDataRequest, error) {
var typedData apitypes.TypedData
if td, ok := data.(apitypes.TypedData); ok {
typedData = td
} else { // Hex-encoded data
jsonData, err := fromHex(data)
if err != nil {
return nil, err
}
if err = json.Unmarshal(jsonData, &typedData); err != nil {
return nil, err
}
}
messages, err := typedData.Format()
if err != nil {
return nil, err
}
sighash, rawData, err := apitypes.TypedDataAndHash(typedData)
if err != nil {
return nil, err
}
return &SignDataRequest{
ContentType: apitypes.DataTyped.Mime,
Rawdata: []byte(rawData),
Messages: messages,
Hash: sighash}, nil
} }
// EcRecover recovers the address associated with the given sig. // EcRecover recovers the address associated with the given sig.
@ -293,30 +324,20 @@ func UnmarshalValidatorData(data interface{}) (apitypes.ValidatorData, error) {
if !ok { if !ok {
return apitypes.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) addrBytes, err := fromHex(raw["address"])
if !ok {
return apitypes.ValidatorData{}, errors.New("validator address is not sent as a string")
}
addrBytes, err := hexutil.Decode(addr)
if err != nil { if err != nil {
return apitypes.ValidatorData{}, err return apitypes.ValidatorData{}, fmt.Errorf("validator address error: %w", err)
} }
if !ok || len(addrBytes) == 0 { if len(addrBytes) == 0 {
return apitypes.ValidatorData{}, errors.New("validator address is undefined") return apitypes.ValidatorData{}, errors.New("validator address is undefined")
} }
messageBytes, err := fromHex(raw["message"])
message, ok := raw["message"].(string)
if !ok {
return apitypes.ValidatorData{}, errors.New("message is not sent as a string")
}
messageBytes, err := hexutil.Decode(message)
if err != nil { if err != nil {
return apitypes.ValidatorData{}, err return apitypes.ValidatorData{}, fmt.Errorf("message error: %w", err)
} }
if !ok || len(messageBytes) == 0 { if len(messageBytes) == 0 {
return apitypes.ValidatorData{}, errors.New("message is undefined") return apitypes.ValidatorData{}, errors.New("message is undefined")
} }
return apitypes.ValidatorData{ return apitypes.ValidatorData{
Address: common.BytesToAddress(addrBytes), Address: common.BytesToAddress(addrBytes),
Message: messageBytes, Message: messageBytes,

@ -220,15 +220,29 @@ func TestSignData(t *testing.T) {
if signature == nil || len(signature) != 65 { if signature == nil || len(signature) != 65 {
t.Errorf("Expected 65 byte signature (got %d bytes)", len(signature)) t.Errorf("Expected 65 byte signature (got %d bytes)", len(signature))
} }
// data/typed // data/typed via SignTypeData
control.approveCh <- "Y" control.approveCh <- "Y"
control.inputCh <- "a_long_password" control.inputCh <- "a_long_password"
signature, err = api.SignTypedData(context.Background(), a, typedData) var want []byte
if err != nil { if signature, err = api.SignTypedData(context.Background(), a, typedData); err != nil {
t.Fatal(err) t.Fatal(err)
} } else if signature == nil || len(signature) != 65 {
if signature == nil || len(signature) != 65 {
t.Errorf("Expected 65 byte signature (got %d bytes)", len(signature)) t.Errorf("Expected 65 byte signature (got %d bytes)", len(signature))
} else {
want = signature
}
// data/typed via SignData / mimetype typed data
control.approveCh <- "Y"
control.inputCh <- "a_long_password"
if typedDataJson, err := json.Marshal(typedData); err != nil {
t.Fatal(err)
} else if signature, err = api.SignData(context.Background(), apitypes.DataTyped.Mime, a, hexutil.Encode(typedDataJson)); err != nil {
t.Fatal(err)
} else if signature == nil || len(signature) != 65 {
t.Errorf("Expected 65 byte signature (got %d bytes)", len(signature))
} else if have := signature; !bytes.Equal(have, want) {
t.Fatalf("want %x, have %x", want, have)
} }
} }