Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new endpoint for generating messages and refactor message generation #1836

Merged
merged 10 commits into from
Mar 20, 2024
257 changes: 1 addition & 256 deletions go/common/viewingkey/viewing_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,58 +3,15 @@ package viewingkey
import (
"crypto/ecdsa"
"encoding/hex"
"errors"
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common/math"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/ecies"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
"github.com/ten-protocol/go-ten/go/wallet"

gethcommon "github.com/ethereum/go-ethereum/common"
)

// SignedMsgPrefix is the prefix added when signing the viewing key in MetaMask using the personal_sign
// API. Why is this needed? MetaMask has a security feature whereby if you ask it to sign something that looks like
// a transaction using the personal_sign API, it modifies the data being signed. The goal is to prevent hackers
// from asking a visitor to their website to personal_sign something that is actually a malicious transaction (e.g.
// theft of funds). By adding a prefix, the viewing key bytes no longer looks like a transaction hash, and thus get
// signed as-is.
const SignedMsgPrefix = "vk"

const (
EIP712Domain = "EIP712Domain"
EIP712Type = "Authentication"
EIP712DomainName = "name"
EIP712DomainVersion = "version"
EIP712DomainChainID = "chainId"
EIP712EncryptionToken = "Encryption Token"
EIP712DomainNameValue = "Ten"
EIP712DomainVersionValue = "1.0"
UserIDHexLength = 40
PersonalSignMessageFormat = "Token: %s on chain: %d version:%d"
)

const (
EIP712Signature SignatureType = 0
PersonalSign SignatureType = 1
Legacy SignatureType = 2
)

// EIP712EncryptionTokens is a list of all possible options for Encryption token name
var EIP712EncryptionTokens = [...]string{
EIP712EncryptionToken,
}

// PersonalSignMessageSupportedVersions is a list of supported versions for the personal sign message
var PersonalSignMessageSupportedVersions = []int{1}

// SignatureType is used to differentiate between different signature types (string is used, because int is not RLP-serializable)
type SignatureType uint8

// ViewingKey encapsulates the signed viewing key for an account for use in encrypted communication with an enclave.
// It is the client-side perspective of the viewing key used for decrypting incoming traffic.
type ViewingKey struct {
Expand All @@ -75,105 +32,6 @@ type RPCSignedViewingKey struct {
SignatureType SignatureType
}

// SignatureChecker is an interface for checking
// if signature is valid for provided encryptionToken and chainID and return singing address or nil if not valid
type SignatureChecker interface {
CheckSignature(encryptionToken string, signature []byte, chainID int64) (*gethcommon.Address, error)
}

type (
PersonalSignChecker struct{}
EIP712Checker struct{}
LegacyChecker struct{}
)

// CheckSignature checks if signature is valid for provided encryptionToken and chainID and return address or nil if not valid
func (psc PersonalSignChecker) CheckSignature(encryptionToken string, signature []byte, chainID int64) (*gethcommon.Address, error) {
if len(signature) != 65 {
return nil, fmt.Errorf("invalid signaure length: %d", len(signature))
}
// We transform the V from 27/28 to 0/1. This same change is made in Geth internals, for legacy reasons to be able
// to recover the address: https://github.com/ethereum/go-ethereum/blob/55599ee95d4151a2502465e0afc7c47bd1acba77/internal/ethapi/api.go#L452-L459
if signature[64] == 27 || signature[64] == 28 {
signature[64] -= 27
}

// create all possible hashes (for all the supported versions) of the message (needed for signature verification)
for _, version := range PersonalSignMessageSupportedVersions {
message := GeneratePersonalSignMessage(encryptionToken, chainID, version)
messageHash := accounts.TextHash([]byte(message))

// current signature is valid - return account address
address, err := CheckSignatureAndReturnAccountAddress(messageHash, signature)
if err == nil {
return address, nil
}
}

return nil, fmt.Errorf("signature verification failed")
}

func (e EIP712Checker) CheckSignature(encryptionToken string, signature []byte, chainID int64) (*gethcommon.Address, error) {
if len(signature) != 65 {
return nil, fmt.Errorf("invalid signaure length: %d", len(signature))
}

rawDataOptions, err := GenerateAuthenticationEIP712RawDataOptions(encryptionToken, chainID)
if err != nil {
return nil, fmt.Errorf("cannot generate eip712 message. Cause %w", err)
}

// We transform the V from 27/28 to 0/1. This same change is made in Geth internals, for legacy reasons to be able
// to recover the address: https://github.com/ethereum/go-ethereum/blob/55599ee95d4151a2502465e0afc7c47bd1acba77/internal/ethapi/api.go#L452-L459
if signature[64] == 27 || signature[64] == 28 {
signature[64] -= 27
}

for _, rawData := range rawDataOptions {
// create a hash of structured message (needed for signature verification)
hashBytes := crypto.Keccak256(rawData)

// current signature is valid - return account address
address, err := CheckSignatureAndReturnAccountAddress(hashBytes, signature)
if err == nil {
return address, nil
}
}
return nil, errors.New("EIP 712 signature verification failed")
}

// CheckSignature checks if signature is valid for provided encryptionToken and chainID and return address or nil if not valid
// todo (@ziga) Remove this method once old WE endpoints are removed
// encryptionToken is expected to be a public key and not encrypted token as with other signature types
// (since this is only temporary fix and legacy format will be removed soon)
func (lsc LegacyChecker) CheckSignature(encryptionToken string, signature []byte, _ int64) (*gethcommon.Address, error) {
publicKey := []byte(encryptionToken)
msgToSignLegacy := GenerateSignMessage(publicKey)

recoveredAccountPublicKeyLegacy, err := crypto.SigToPub(accounts.TextHash([]byte(msgToSignLegacy)), signature)
if err != nil {
return nil, fmt.Errorf("failed to recover account public key from legacy signature: %w", err)
}
recoveredAccountAddressLegacy := crypto.PubkeyToAddress(*recoveredAccountPublicKeyLegacy)
return &recoveredAccountAddressLegacy, nil
}

// SignatureChecker is a map of SignatureType to SignatureChecker
var signatureCheckers = map[SignatureType]SignatureChecker{
PersonalSign: PersonalSignChecker{},
EIP712Signature: EIP712Checker{},
Legacy: LegacyChecker{},
}

// CheckSignature checks if signature is valid for provided encryptionToken and chainID and return address or nil if not valid
func CheckSignature(encryptionToken string, signature []byte, chainID int64, signatureType SignatureType) (*gethcommon.Address, error) {
checker, exists := signatureCheckers[signatureType]
if !exists {
return nil, fmt.Errorf("unsupported signature type")
}
return checker.CheckSignature(encryptionToken, signature, chainID)
}

// GenerateViewingKeyForWallet takes an account wallet, generates a viewing key and signs the key with the acc's private key
// uses the same method of signature handling as Metamask/geth
// TODO @Ziga - update this method to use the new EIP-712 signature format / personal sign after the removal of the legacy format
Expand Down Expand Up @@ -241,116 +99,3 @@ func Sign(userPrivKey *ecdsa.PrivateKey, vkPubKey []byte) ([]byte, error) {
}
return signature, nil
}

// GenerateSignMessage creates the message to be signed
// vkPubKey is expected to be a []byte("0x....") to create the signing message
// todo (@ziga) Remove this method once old WE endpoints are removed
func GenerateSignMessage(vkPubKey []byte) string {
return SignedMsgPrefix + hex.EncodeToString(vkPubKey)
}

func GeneratePersonalSignMessage(encryptionToken string, chainID int64, version int) string {
return fmt.Sprintf(PersonalSignMessageFormat, encryptionToken, chainID, version)
}

// getBytesFromTypedData creates EIP-712 compliant hash from typedData.
// It involves hashing the message with its structure, hashing domain separator,
// and then encoding both hashes with specific EIP-712 bytes to construct the final message format.
func getBytesFromTypedData(typedData apitypes.TypedData) ([]byte, error) {
typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message)
if err != nil {
return nil, err
}
// Create the domain separator hash for EIP-712 message context
domainSeparator, err := typedData.HashStruct(EIP712Domain, typedData.Domain.Map())
if err != nil {
return nil, err
}
// Prefix domain and message hashes with EIP-712 version and encoding bytes
rawData := append([]byte("\x19\x01"), append(domainSeparator, typedDataHash...)...)
return rawData, nil
}

// GenerateAuthenticationEIP712RawDataOptions generates all the options or raw data messages (bytes)
// for an EIP-712 message used to authenticate an address with user
// (currently only one option is supported, but function leaves room for future expansion of options)
func GenerateAuthenticationEIP712RawDataOptions(userID string, chainID int64) ([][]byte, error) {
if len(userID) != UserIDHexLength {
return nil, fmt.Errorf("userID hex length must be %d, received %d", UserIDHexLength, len(userID))
}
encryptionToken := "0x" + userID

domain := apitypes.TypedDataDomain{
Name: EIP712DomainNameValue,
Version: EIP712DomainVersionValue,
ChainId: (*math.HexOrDecimal256)(big.NewInt(chainID)),
}

message := map[string]interface{}{
EIP712EncryptionToken: encryptionToken,
}

types := apitypes.Types{
EIP712Domain: {
{Name: EIP712DomainName, Type: "string"},
{Name: EIP712DomainVersion, Type: "string"},
{Name: EIP712DomainChainID, Type: "uint256"},
},
EIP712Type: {
{Name: EIP712EncryptionToken, Type: "address"},
},
}

newTypeElement := apitypes.TypedData{
Types: types,
PrimaryType: EIP712Type,
Domain: domain,
Message: message,
}

rawDataOptions := make([][]byte, 0)
rawData, err := getBytesFromTypedData(newTypeElement)
if err != nil {
return nil, err
}
rawDataOptions = append(rawDataOptions, rawData)

return rawDataOptions, nil
}

// CalculateUserIDHex CalculateUserID calculates userID from a public key
// (we truncate it, because we want it to have length 20) and encode to hex strings
func CalculateUserIDHex(publicKeyBytes []byte) string {
return hex.EncodeToString(CalculateUserID(publicKeyBytes))
}

// CalculateUserID calculates userID from a public key (we truncate it, because we want it to have length 20)
func CalculateUserID(publicKeyBytes []byte) []byte {
return crypto.Keccak256Hash(publicKeyBytes).Bytes()[:20]
}

// CheckSignatureAndReturnAccountAddress checks if the signature is valid for hash of the message and checks if
// signer is an address provided to the function.
// It returns an address if the signature is valid and nil otherwise
func CheckSignatureAndReturnAccountAddress(hashBytes []byte, signature []byte) (*gethcommon.Address, error) {
pubKeyBytes, err := crypto.Ecrecover(hashBytes, signature)
if err != nil {
return nil, err
}

pubKey, err := crypto.UnmarshalPubkey(pubKeyBytes)
if err != nil {
return nil, err
}

r := new(big.Int).SetBytes(signature[:32])
s := new(big.Int).SetBytes(signature[32:64])

// Verify the signature and return the result (all the checks above passed)
isSigValid := ecdsa.Verify(pubKey, hashBytes, r, s)
if isSigValid {
address := crypto.PubkeyToAddress(*pubKey)
return &address, nil
}
return nil, fmt.Errorf("invalid signature")
}
Loading
Loading