Skip to content

Commit

Permalink
signer: enable typed data signing from signer rpc (ethereum#26241)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
2 people authored and shekhirin committed Jun 6, 2023
1 parent da054e9 commit 2f38d8d
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 54 deletions.
121 changes: 71 additions & 50 deletions signer/core/signed_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package core

import (
"context"
"encoding/json"
"errors"
"fmt"
"mime"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
22 changes: 18 additions & 4 deletions signer/core/signed_data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
} else if signature == nil || len(signature) != 65 {
t.Errorf("Expected 65 byte signature (got %d bytes)", len(signature))
} else {
want = signature
}
if signature == nil || len(signature) != 65 {

// 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)
}
}

Expand Down

0 comments on commit 2f38d8d

Please sign in to comment.