Skip to content

Commit

Permalink
Add new endpoint for generating messages and refactor message generat…
Browse files Browse the repository at this point in the history
…ion (#1836)
  • Loading branch information
zkokelj authored Mar 20, 2024
1 parent abcfc38 commit 2b17a1e
Show file tree
Hide file tree
Showing 8 changed files with 530 additions and 298 deletions.
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

0 comments on commit 2b17a1e

Please sign in to comment.