Stefan dc62171219 fix(dapps)_: don't require chain ID for signing typed data v4
The reused implementation from signing typed data V1 was used
in case of signing typed data V4. This implementation required
chain ID to be present in the typed data. This change fixes
the issue by making chainID optional for signing typed data V4.
2024-07-19 09:00:01 +02:00

292 lines
8.6 KiB
Go

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"
)
const (
SupportedEip155Namespace = "eip155"
ProposeUserPairEvent = walletevent.EventType("WalletConnectProposeUserPair")
)
var (
ErrorInvalidSessionProposal = errors.New("invalid session proposal")
ErrorNamespaceNotSupported = errors.New("namespace not supported")
ErrorChainsNotSupported = errors.New("chains not supported")
ErrorInvalidParamsCount = errors.New("invalid params count")
ErrorInvalidAddressMsgIndex = errors.New("invalid address and/or msg index (must be 0 or 1)")
ErrorMethodNotSupported = errors.New("method not supported")
)
type Topic string
type Namespace struct {
Methods []string `json:"methods"`
Chains []string `json:"chains"` // CAIP-2 format e.g. ["eip155:1"]
Events []string `json:"events"`
Accounts []string `json:"accounts,omitempty"` // CAIP-10 format e.g. ["eip155:1:0x453...228"]
}
type Metadata struct {
Description string `json:"description"`
URL string `json:"url"`
Icons []string `json:"icons"`
Name string `json:"name"`
VerifyURL string `json:"verifyUrl"`
}
type Proposer struct {
PublicKey string `json:"publicKey"`
Metadata Metadata `json:"metadata"`
}
type Verified struct {
VerifyURL string `json:"verifyUrl"`
Validation string `json:"validation"`
Origin string `json:"origin"`
IsScam bool `json:"isScam,omitempty"`
}
type VerifyContext struct {
Verified Verified `json:"verified"`
}
// Params has RequiredNamespaces entries if part of "proposal namespace" and Namespaces entries if part of "session namespace"
// see https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#controller-side-validation-of-incoming-proposal-namespaces-wallet
type Params struct {
ID int64 `json:"id"`
PairingTopic Topic `json:"pairingTopic"`
Expiry int64 `json:"expiry"`
RequiredNamespaces map[string]Namespace `json:"requiredNamespaces"`
OptionalNamespaces map[string]Namespace `json:"optionalNamespaces"`
Proposer Proposer `json:"proposer"`
Verify VerifyContext `json:"verifyContext"`
}
type SessionProposal struct {
ID int64 `json:"id"`
Params Params `json:"params"`
}
type PairSessionResponse struct {
SessionProposal SessionProposal `json:"sessionProposal"`
SupportedNamespaces map[string]Namespace `json:"supportedNamespaces"`
}
type RequestParams struct {
Request struct {
Method string `json:"method"`
Params []json.RawMessage `json:"params"`
} `json:"request"`
ChainID string `json:"chainId"`
}
type SessionRequest struct {
ID int64 `json:"id"`
Topic Topic `json:"topic"`
Params RequestParams `json:"params"`
Verify VerifyContext `json:"verifyContext"`
}
type SessionDelete struct {
ID int64 `json:"id"`
Topic Topic `json:"topic"`
}
type Session struct {
Acknowledged bool `json:"acknowledged"`
Controller string `json:"controller"`
Expiry int64 `json:"expiry"`
Namespaces map[string]Namespace `json:"namespaces"`
OptionalNamespaces map[string]Namespace `json:"optionalNamespaces"`
PairingTopic Topic `json:"pairingTopic"`
Peer Proposer `json:"peer"`
Relay json.RawMessage `json:"relay"`
RequiredNamespaces map[string]Namespace `json:"requiredNamespaces"`
Self Proposer `json:"self"`
Topic Topic `json:"topic"`
}
// Valid namespace
func (n *Namespace) Valid(namespaceName string, chainID *uint64) bool {
if chainID == nil {
if len(n.Chains) == 0 {
log.Warn("namespace doesn't refer to any chain")
return false
}
for _, caip2Str := range n.Chains {
resolvedNamespaceName, _, err := parseCaip2ChainID(caip2Str)
if err != nil {
log.Warn("namespace chain not in caip2 format", "chain", caip2Str, "error", err)
return false
}
if resolvedNamespaceName != namespaceName {
log.Warn("namespace name doesn't match", "namespace", namespaceName, "chain", caip2Str)
return false
}
}
}
return true
}
// ValidateForProposal validates params part of the Proposal Namespace
func (p *Params) ValidateForProposal() bool {
for key, ns := range p.RequiredNamespaces {
var chainID *uint64
if strings.Contains(key, ":") {
resolvedNamespaceName, cID, err := parseCaip2ChainID(key)
if err != nil {
log.Warn("params validation failed CAIP-2", "str", key, "error", err)
return false
}
key = resolvedNamespaceName
chainID = &cID
}
if !isValidNamespaceName(key) {
log.Warn("invalid namespace name", "namespace", key)
return false
}
if !ns.Valid(key, chainID) {
return false
}
}
return true
}
// ValidateProposal validates params part of the Proposal Namespace
// https://specs.walletconnect.com/2.0/specs/clients/sign/namespaces#controller-side-validation-of-incoming-proposal-namespaces-wallet
func (p *SessionProposal) ValidateProposal() bool {
return p.Params.ValidateForProposal()
}
// AddSession adds a new active session to the database
func AddSession(db *sql.DB, networks []params.Network, session_json string) error {
var session Session
err := json.Unmarshal([]byte(session_json), &session)
if err != nil {
return fmt.Errorf("unmarshal session: %v", err)
}
chains := supportedChainsInSession(session)
testChains, err := areTestChains(networks, chains)
if err != nil {
return fmt.Errorf("areTestChains: %v", err)
}
rowEntry := DBSession{
Topic: session.Topic,
Disconnected: false,
SessionJSON: session_json,
Expiry: session.Expiry,
CreatedTimestamp: time.Now().Unix(),
PairingTopic: session.PairingTopic,
TestChains: testChains,
DBDApp: DBDApp{
URL: session.Peer.Metadata.URL,
Name: session.Peer.Metadata.Name,
},
}
if len(session.Peer.Metadata.Icons) > 0 {
rowEntry.IconURL = session.Peer.Metadata.Icons[0]
}
return UpsertSession(db, rowEntry)
}
// areTestChains assumes chains to tests are all testnets or all mainnets
func areTestChains(networks []params.Network, chainIDs []uint64) (isTest bool, err error) {
for _, n := range networks {
for _, chainID := range chainIDs {
if n.ChainID == chainID {
return n.IsTest, nil
}
}
}
return false, fmt.Errorf("no network found for chainIDs %v", chainIDs)
}
func supportedChainsInSession(session Session) []uint64 {
caipChains := session.Namespaces[SupportedEip155Namespace].Chains
chains := make([]uint64, 0, len(caipChains))
for _, caip2Str := range caipChains {
_, chainID, err := parseCaip2ChainID(caip2Str)
if err != nil {
log.Warn("Failed parsing CAIP-2", "str", caip2Str, "error", err)
continue
}
chains = append(chains, chainID)
}
return chains
}
func caip10Accounts(accounts []*accounts.Account, chains []uint64) []string {
addresses := make([]string, 0, len(accounts)*len(chains))
for _, acc := range accounts {
for _, chainID := range chains {
addresses = append(addresses, fmt.Sprintf("%s:%s:%s", SupportedEip155Namespace, strconv.FormatUint(chainID, 10), acc.Address.Hex()))
}
}
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)
var sig hexutil.Bytes
if legacy {
sig, err = typeddata.Sign(typed, privateKey, chain)
} else {
// Validate chainID if part of the typed data
if _, exist := typed.Domain[typeddata.ChainIDKey]; exist {
if err := typed.ValidateChainID(chain); err != nil {
return types.HexBytes{}, err
}
}
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
}