From 6a4e05c93accb11d16037bf92534ed57a84f9394 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Fri, 25 Nov 2022 09:13:45 +0100 Subject: [PATCH] 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 --- signer/core/signed_data.go | 121 +++++++++++++++++++------------- signer/core/signed_data_test.go | 22 ++++-- 2 files changed, 89 insertions(+), 54 deletions(-) diff --git a/signer/core/signed_data.go b/signer/core/signed_data.go index c0da22e626..8ee572f53e 100644 --- a/signer/core/signed_data.go +++ b/signer/core/signed_data.go @@ -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, diff --git a/signer/core/signed_data_test.go b/signer/core/signed_data_test.go index 7d5661e7e6..8deff919cb 100644 --- a/signer/core/signed_data_test.go +++ b/signer/core/signed_data_test.go @@ -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) } }