mirror of
https://github.com/status-im/status-go.git
synced 2025-01-14 16:56:44 +00:00
dc62171219
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.
292 lines
8.6 KiB
Go
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
|
|
}
|