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

View File

@ -18,6 +18,7 @@ package core
import (
"context"
"encoding/json"
"errors"
"fmt"
"mime"
@ -135,11 +136,7 @@ func (api *SignerAPI) determineSignatureFormat(ctx context.Context, contentType
req = &SignDataRequest{ContentType: mediaType, Rawdata: []byte(msg), Messages: messages, Hash: sighash}
case apitypes.ApplicationClique.Mime:
// Clique is the Ethereum PoA standard
stringData, ok := data.(string)
if !ok {
return nil, useEthereumV, fmt.Errorf("input for %v must be an hex-encoded string", apitypes.ApplicationClique.Mime)
}
cliqueData, err := hexutil.Decode(stringData)
cliqueData, err := fromHex(data)
if err != nil {
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
useEthereumV = false
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:
// Calculates an Ethereum ECDSA signature for:
// hash = keccak256("\x19Ethereum Signed Message:\n${message length}${message}")
// We expect it to be a string
if stringData, ok := data.(string); !ok {
return nil, useEthereumV, fmt.Errorf("input for text/plain must be an hex-encoded string")
} else {
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}
}
// We expect input to be a hex-encoded string
textData, err := fromHex(data)
if err != nil {
return nil, useEthereumV, err
}
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.Meta = MetadataFromContext(ctx)
@ -233,20 +233,12 @@ func (api *SignerAPI) SignTypedData(ctx context.Context, addr common.MixedcaseAd
// - the signature preimage (hash)
func (api *SignerAPI) signTypedData(ctx context.Context, addr common.MixedcaseAddress,
typedData apitypes.TypedData, validationMessages *apitypes.ValidationMessages) (hexutil.Bytes, hexutil.Bytes, error) {
sighash, rawData, err := apitypes.TypedDataAndHash(typedData)
req, err := typedDataRequest(typedData)
if err != nil {
return nil, nil, err
}
messages, err := typedData.Format()
if err != nil {
return nil, nil, err
}
req := &SignDataRequest{
ContentType: apitypes.DataTyped.Mime,
Rawdata: []byte(rawData),
Messages: messages,
Hash: sighash,
Address: addr}
req.Address = addr
req.Meta = MetadataFromContext(ctx)
if validationMessages != nil {
req.Callinfo = validationMessages.Messages
}
@ -255,7 +247,46 @@ func (api *SignerAPI) signTypedData(ctx context.Context, addr common.MixedcaseAd
api.UI.ShowError(err.Error())
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.
@ -293,30 +324,20 @@ func UnmarshalValidatorData(data interface{}) (apitypes.ValidatorData, error) {
if !ok {
return apitypes.ValidatorData{}, errors.New("validator input is not a map[string]interface{}")
}
addr, ok := raw["address"].(string)
if !ok {
return apitypes.ValidatorData{}, errors.New("validator address is not sent as a string")
}
addrBytes, err := hexutil.Decode(addr)
addrBytes, err := fromHex(raw["address"])
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")
}
message, ok := raw["message"].(string)
if !ok {
return apitypes.ValidatorData{}, errors.New("message is not sent as a string")
}
messageBytes, err := hexutil.Decode(message)
messageBytes, err := fromHex(raw["message"])
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{
Address: common.BytesToAddress(addrBytes),
Message: messageBytes,

View File

@ -220,15 +220,29 @@ func TestSignData(t *testing.T) {
if signature == nil || len(signature) != 65 {
t.Errorf("Expected 65 byte signature (got %d bytes)", len(signature))
}
// data/typed
// data/typed via SignTypeData
control.approveCh <- "Y"
control.inputCh <- "a_long_password"
signature, err = api.SignTypedData(context.Background(), a, typedData)
if err != nil {
var want []byte
if signature, err = api.SignTypedData(context.Background(), a, typedData); err != nil {
t.Fatal(err)
}
if signature == nil || len(signature) != 65 {
} else if signature == nil || len(signature) != 65 {
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)
}
}