feat(dapps)_: extend and improve sign

Add `wallet_SafeSignTypedDataForDApps` with support for
`eth_signTypedData` and `eth_signTypedData_v4`
Reject if the chain to sign doesn't matches the target chain
for typed data signing

Add `wallet_HashMessageForSigning` with to support hashing messages
for signing in a safe way as per EIP-191 v45 and supporting
to hash messages for signing on the client side (keycard)

Deprecate `wallet_SignTypedDataV4``

Updates: #15361
This commit is contained in:
Stefan 2024-07-03 00:46:47 +03:00 committed by Stefan Dunca
parent 3145ab05ff
commit 5336c47f1b
4 changed files with 208 additions and 4 deletions

View File

@ -16,6 +16,7 @@ import (
gethrpc "github.com/ethereum/go-ethereum/rpc"
signercore "github.com/ethereum/go-ethereum/signer/core/apitypes"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/rpc/network"
@ -853,8 +854,20 @@ func (api *API) GetWalletConnectDapps(ctx context.Context, validAtTimestamp int6
return walletconnect.GetActiveDapps(api.s.db, validAtTimestamp, testChains)
}
// signTypedDataV4 dApps use it to execute "eth_signTypedData_v4" requests
// HashMessageEIP191 is used for hashing dApps requests for "personal_sign" and "eth_sign"
// in a safe manner following the EIP-191 version 0x45 for signing on the client side.
func (api *API) HashMessageEIP191(ctx context.Context, message types.HexBytes) types.Hash {
log.Debug("wallet.api.HashMessageEIP191", "len(data)", len(message))
safeMsg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(message), string(message))
return crypto.Keccak256Hash([]byte(safeMsg))
}
// SignTypedDataV4 dApps use it to execute "eth_signTypedData_v4" requests
// the formatted typed data will be prefixed with \x19\x01 based on the EIP-712
// @deprecated
func (api *API) SignTypedDataV4(typedJson string, address string, password string) (types.HexBytes, error) {
log.Debug("wallet.api.SignTypedDataV4", "len(typedJson)", len(typedJson), "address", address, "len(password)", len(password))
account, err := api.getVerifiedWalletAccount(address, password)
if err != nil {
return types.HexBytes{}, err
@ -874,6 +887,22 @@ func (api *API) SignTypedDataV4(typedJson string, address string, password strin
return types.HexBytes(sig), err
}
// SafeSignTypedDataForDApps is used to execute requests for "eth_signTypedData"
// if legacy is true else "eth_signTypedData_v4"
// the formatted typed data won't be prefixed in case of legacy calls, as the
// old dApps implementation expects
// the chain is validate for both cases
func (api *API) SafeSignTypedDataForDApps(typedJson string, address string, password string, chainID uint64, legacy bool) (types.HexBytes, error) {
log.Debug("wallet.api.SafeSignTypedDataForDApps", "len(typedJson)", len(typedJson), "address", address, "len(password)", len(password), "chainID", chainID, "legacy", legacy)
account, err := api.getVerifiedWalletAccount(address, password)
if err != nil {
return types.HexBytes{}, err
}
return walletconnect.SafeSignTypedDataForDApps(typedJson, account.AccountKey.PrivateKey, chainID, legacy)
}
func (api *API) RestartWalletReloadTimer(ctx context.Context) error {
return api.s.reader.Restart()
}

View File

@ -21,3 +21,11 @@ func TestAPI_GetWalletConnectActiveSessions(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 0, len(sessions))
}
// TestAPI_HashMessageEIP191
func TestAPI_HashMessageEIP191(t *testing.T) {
api := &API{}
res := api.HashMessageEIP191(context.Background(), []byte("test"))
require.Equal(t, "0x4a5c5d454721bbbb25540c3317521e71c373ae36458f960d2ad46ef088110e95", res.String())
}

View File

@ -1,18 +1,24 @@
package walletconnect
import (
"crypto/ecdsa"
"database/sql"
"encoding/json"
"errors"
"fmt"
"math/big"
"strconv"
"strings"
"time"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/log"
signercore "github.com/ethereum/go-ethereum/signer/core/apitypes"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/services/typeddata"
"github.com/status-im/status-go/services/wallet/walletevent"
)
@ -247,3 +253,35 @@ func caip10Accounts(accounts []*accounts.Account, chains []uint64) []string {
}
return addresses
}
func SafeSignTypedDataForDApps(typedJson string, privateKey *ecdsa.PrivateKey, chainID uint64, legacy bool) (types.HexBytes, error) {
// Parse the data for both legacy and non-legacy cases to validate the chain
var typed typeddata.TypedData
err := json.Unmarshal([]byte(typedJson), &typed)
if err != nil {
return types.HexBytes{}, err
}
chain := new(big.Int).SetUint64(chainID)
if err := typed.ValidateChainID(chain); err != nil {
return types.HexBytes{}, err
}
var sig hexutil.Bytes
if legacy {
sig, err = typeddata.Sign(typed, privateKey, chain)
} else {
var typedV4 signercore.TypedData
err = json.Unmarshal([]byte(typedJson), &typedV4)
if err != nil {
return types.HexBytes{}, err
}
sig, err = typeddata.SignTypedDataV4(typedV4, privateKey, chain)
}
if err != nil {
return types.HexBytes{}, err
}
return types.HexBytes(sig), err
}

View File

@ -1,18 +1,20 @@
package walletconnect
import (
"crypto/ecdsa"
"encoding/json"
"reflect"
"strconv"
"testing"
"time"
"encoding/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/params"
"github.com/stretchr/testify/assert"
)
func getSessionJSONFor(chains []int, expiry int) string {
@ -425,3 +427,130 @@ func Test_AddSession(t *testing.T) {
assert.Equal(t, sessions[0].Name, dapps[0].Name)
assert.Equal(t, sessions[0].IconURL, dapps[0].IconURL)
}
func generateTypedDataJson(chainID int, skipField bool) string {
optionalKeyValueField := ""
if !skipField {
optionalKeyValueField = `,"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"`
}
typedData := `{
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"Person": [
{"name": "name", "type": "string"},
{"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": ` + strconv.Itoa(chainID) + `
` + optionalKeyValueField + `
},
"message": {
"from": {
"name": "Cow",
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
},
"to": {
"name": "Bob",
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
},
"contents": "Hello, Bob!"
}
}`
return typedData
}
func TestSafeSignTypedDataForDApps(t *testing.T) {
// 0x4f1B9Ee595bF612480ADAF623Ec583f623ae802d
privateKey, err := crypto.HexToECDSA("efe79ae971aa8bb612de9de7c65b9224ab1b6a69e6ec733ec92110f100c7244a")
require.NoError(t, err)
type args struct {
typedJson string
privateKey *ecdsa.PrivateKey
chainID uint64
legacy bool
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "sign_typed_data",
args: args{
typedJson: generateTypedDataJson(1, false),
privateKey: privateKey,
chainID: 1,
legacy: false,
},
wantErr: false,
},
{
name: "sign_typed_data_legacy",
args: args{
typedJson: generateTypedDataJson(1, false),
privateKey: privateKey,
chainID: 1,
legacy: true,
},
wantErr: false,
},
{
name: "sign_typed_data_invalid_json",
args: args{
typedJson: `{"invalid": "json"`,
privateKey: privateKey,
chainID: 1,
legacy: false,
},
wantErr: true,
},
{
name: "sign_typed_data_invalid_chain_id",
args: args{
typedJson: generateTypedDataJson(1, false),
privateKey: privateKey,
chainID: 2,
legacy: false,
},
wantErr: true,
},
{
name: "sign_typed_data_missing_field",
args: args{
typedJson: generateTypedDataJson(1, true),
privateKey: privateKey,
chainID: 1,
legacy: false,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := SafeSignTypedDataForDApps(tt.args.typedJson, tt.args.privateKey, tt.args.chainID, tt.args.legacy)
if (err != nil) != tt.wantErr {
t.Errorf("SafeSignTypedDataForDApps() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
require.NotEmpty(t, got)
require.Len(t, got, 65)
}
})
}
}