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
144 changes: 87 additions & 57 deletions go/common/viewingkey/viewing_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package viewingkey
import (
"crypto/ecdsa"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"math/big"
Expand Down Expand Up @@ -100,8 +101,10 @@ func (psc PersonalSignChecker) CheckSignature(encryptionToken string, signature

// 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))
messageHash, err := GenerateMessage(encryptionToken, chainID, version, PersonalSign, true)
if err != nil {
return nil, fmt.Errorf("cannot generate message. Cause %w", err)
}

// current signature is valid - return account address
address, err := CheckSignatureAndReturnAccountAddress(messageHash, signature)
Expand All @@ -118,9 +121,9 @@ func (e EIP712Checker) CheckSignature(encryptionToken string, signature []byte,
return nil, fmt.Errorf("invalid signaure length: %d", len(signature))
}

rawDataOptions, err := GenerateAuthenticationEIP712RawDataOptions(encryptionToken, chainID)
messageHash, err := GenerateMessage(encryptionToken, chainID, 1, EIP712Signature, true)
if err != nil {
return nil, fmt.Errorf("cannot generate eip712 message. Cause %w", err)
return nil, fmt.Errorf("cannot generate 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
Expand All @@ -129,16 +132,12 @@ func (e EIP712Checker) CheckSignature(encryptionToken string, signature []byte,
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
}
// current signature is valid - return account address
address, err := CheckSignatureAndReturnAccountAddress(messageHash, signature)
if err == nil {
return address, nil
}

return nil, errors.New("EIP 712 signature verification failed")
}

Expand Down Expand Up @@ -249,10 +248,6 @@ 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.
Expand All @@ -271,14 +266,71 @@ func getBytesFromTypedData(typedData apitypes.TypedData) ([]byte, error) {
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))
// 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
}
encryptionToken := "0x" + userID
return nil, fmt.Errorf("invalid signature")
}

type MessageGenerator interface {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's move these types to a separate file in the same package.
The viewing_key one is getting a bit large

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we're at it, let's move the signing checking in a separate file as well

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the "GenerateViewingKeyForWallet" logic with the 2 utility methods it uses

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, added 2 new files:

  • viewing_key_messages.go // for all the constants and functions related to generating messages
  • viewing_key_signature.go // for all the code related to checking signatures

And it is much clearer now and easier to see things.

generateMessage(encryptionToken string, chainID int64, version int, hash bool) ([]byte, error)
}

type (
PersonalMessageGenerator struct{}
EIP712MessageGenerator struct{}
)

var messageGenerators = map[SignatureType]MessageGenerator{
PersonalSign: PersonalMessageGenerator{},
EIP712Signature: EIP712MessageGenerator{},
}

// GenerateMessage generates a message for the given encryptionToken, chainID, version and signatureType
func (p PersonalMessageGenerator) generateMessage(encryptionToken string, chainID int64, version int, hash bool) ([]byte, error) {
textMessage := fmt.Sprintf(PersonalSignMessageFormat, encryptionToken, chainID, version)
if hash {
return accounts.TextHash([]byte(textMessage)), nil
}
return []byte(textMessage), nil
}

func (e EIP712MessageGenerator) generateMessage(encryptionToken string, chainID int64, _ int, hash bool) ([]byte, error) {
if len(encryptionToken) != UserIDHexLength {
return nil, fmt.Errorf("userID hex length must be %d, received %d", UserIDHexLength, len(encryptionToken))
}
encryptionToken = "0x" + encryptionToken

domain := apitypes.TypedDataDomain{
Name: EIP712DomainNameValue,
Expand Down Expand Up @@ -308,49 +360,27 @@ func GenerateAuthenticationEIP712RawDataOptions(userID string, chainID int64) ([
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)
// add the JSON message to the list of messages
jsonData, err := json.Marshal(newTypeElement)
if err != nil {
return nil, err
}

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

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
func GenerateMessage(encryptionToken string, chainID int64, version int, signatureType SignatureType, hash bool) ([]byte, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs a doc. i can't figure out what "hash" is doing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment.
The with the last parameter se to true we can get hashed version of the message (so the prefixes for personal sign is already taken care of, etc..). I noticed we used to get message and then hash it in multiple places in our code and decided to simplify it a bit and do it on only one place.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, but I don't think it's a good idea because it mixes responsibilities.
Let's keep the method to always return the message - basically keep that its single responsibility.
And when you need hashes, you call the method and then hash explicitly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. It has a single responsibility + create a new function to create hash for each message

generator, exists := messageGenerators[signatureType]
if !exists {
return nil, fmt.Errorf("unsupported signature type")
}
return nil, fmt.Errorf("invalid signature")
return generator.generateMessage(encryptionToken, chainID, version, hash)
}
19 changes: 11 additions & 8 deletions go/enclave/vkhandler/vk_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (

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

"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/ecies"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -49,13 +48,14 @@ func TestCheckSignature(t *testing.T) {
userPrivKey, _, userID, userAddress := generateRandomUserKeys()

// Generate all message types and create map with the corresponding signature type
// Test EIP712 message format
EIP712MessageDataOptions, err := viewingkey.GenerateAuthenticationEIP712RawDataOptions(userID, chainID)
EIP712MessageHash, err := viewingkey.GenerateMessage(userID, chainID, 0, viewingkey.EIP712Signature, true)
if err != nil {
t.Fatalf(err.Error())
}
PersonalSignMessageHash, err := viewingkey.GenerateMessage(userID, chainID, viewingkey.PersonalSignMessageSupportedVersions[0], viewingkey.PersonalSign, true)
if err != nil {
t.Fatalf(err.Error())
}
EIP712MessageHash := crypto.Keccak256(EIP712MessageDataOptions[0])
PersonalSignMessageHash := accounts.TextHash([]byte(viewingkey.GeneratePersonalSignMessage(userID, chainID, viewingkey.PersonalSignMessageSupportedVersions[0])))

messages := map[string]MessageWithSignatureType{
"EIP712MessageHash": {
Expand Down Expand Up @@ -86,12 +86,15 @@ func TestVerifyViewingKey(t *testing.T) {
userPrivKey, vkPrivKey, userID, userAddress := generateRandomUserKeys()
// Generate all message types and create map with the corresponding signature type
// Test EIP712 message format
EIP712MessageDataOptions, err := viewingkey.GenerateAuthenticationEIP712RawDataOptions(userID, chainID)

EIP712MessageHash, err := viewingkey.GenerateMessage(userID, chainID, viewingkey.PersonalSignMessageSupportedVersions[0], viewingkey.EIP712Signature, true)
if err != nil {
t.Fatalf(err.Error())
}
PersonalSignMessageHash, err := viewingkey.GenerateMessage(userID, chainID, viewingkey.PersonalSignMessageSupportedVersions[0], viewingkey.PersonalSign, true)
if err != nil {
t.Fatalf(err.Error())
}
EIP712MessageHash := crypto.Keccak256(EIP712MessageDataOptions[0])
PersonalSignMessageHash := accounts.TextHash([]byte(viewingkey.GeneratePersonalSignMessage(userID, chainID, viewingkey.PersonalSignMessageSupportedVersions[0])))

messages := map[string]MessageWithSignatureType{
"EIP712MessageHash": {
Expand Down
85 changes: 85 additions & 0 deletions tools/walletextension/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ func NewHTTPRoutes(walletExt *walletextension.WalletExtension) []Route {
Name: common.APIVersion1 + common.PathJoin,
Func: httpHandler(walletExt, joinRequestHandler),
},
{
Name: common.APIVersion1 + common.PathGetMessage,
Func: httpHandler(walletExt, getMessageRequestHandler),
},
{
Name: common.APIVersion1 + common.PathAuthenticate,
Func: httpHandler(walletExt, authenticateRequestHandler),
Expand Down Expand Up @@ -490,3 +494,84 @@ func versionRequestHandler(walletExt *walletextension.WalletExtension, userConn
walletExt.Logger().Error("error writing success response", log.ErrKey, err)
}
}

// ethRequestHandler parses the user eth request, passes it on to the WE to proxy it and processes the response
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment not correc

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

func getMessageRequestHandler(walletExt *walletextension.WalletExtension, conn userconn.UserConn) {
// read the request
body, err := conn.ReadRequest()
if err != nil {
handleError(conn, walletExt.Logger(), fmt.Errorf("error reading request: %w", err))
return
}

// read body of the request
var reqJSONMap map[string]interface{}
err = json.Unmarshal(body, &reqJSONMap)
if err != nil {
handleError(conn, walletExt.Logger(), fmt.Errorf("could not unmarshal address request - %w", err))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this error correct?
What is the format of the body?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed error message. In the body we have "encryptionToken" and optionally "formats".

return
}

// get address from the request
encryptionToken, ok := reqJSONMap[common.JSONKeyEncryptionToken]
if !ok || len(encryptionToken.(string)) != common.MessageUserIDLen {
handleError(conn, walletExt.Logger(), fmt.Errorf("unable to read encryptionToken field from the request or it is not of correct length"))
return
}

// get formats from the request, if present
var formatsSlice []string
if formatsInterface, ok := reqJSONMap[common.JSONKeyFormats]; ok {
formats, ok := formatsInterface.([]interface{})
if !ok {
handleError(conn, walletExt.Logger(), fmt.Errorf("formats field is not an array"))
return
}

for _, f := range formats {
formatStr, ok := f.(string)
if !ok {
handleError(conn, walletExt.Logger(), fmt.Errorf("format value is not a string"))
return
}
formatsSlice = append(formatsSlice, formatStr)
}
}

message, err := walletExt.GetMessage(encryptionToken.(string), formatsSlice)
if err != nil {
handleError(conn, walletExt.Logger(), fmt.Errorf("internal error"))
walletExt.Logger().Error("error getting message", log.ErrKey, err)
return
}

// create the response structure
type JSONResponse struct {
Message string `json:"message"`
Type string `json:"type"`
}

// get string representation of the message format
messageFormat := common.GetBestFormat(formatsSlice)
messageFormatString, err := common.GetSignatureTypeString(messageFormat)
if err != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this error is not really possible, since you're doing the conversion and passing in the argument

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

handleError(conn, walletExt.Logger(), fmt.Errorf("internal error"))
return
}

response := JSONResponse{
Message: message,
Type: messageFormatString,
}

responseBytes, err := json.Marshal(response)
if err != nil {
walletExt.Logger().Error("error marshaling JSON response", log.ErrKey, err)
return
}

err = conn.WriteResponse(responseBytes)
if err != nil {
walletExt.Logger().Error("error writing success response", log.ErrKey, err)
}
}
20 changes: 20 additions & 0 deletions tools/walletextension/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,23 @@ func NewFileLogger() gethlog.Logger {

return logger
}

// GetBestFormat returns the best format for a message based on available formats that are supported by the user
func GetBestFormat(formatsSlice []string) viewingkey.SignatureType {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should live in the same file with the other message generator stuff

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved

// If "Personal" is the only format available, choose it
if len(formatsSlice) == 1 && formatsSlice[0] == "Personal" {
return viewingkey.PersonalSign
}

// otherwise, choose EIP712
return viewingkey.EIP712Signature
}

func GetSignatureTypeString(expectedSignatureType viewingkey.SignatureType) (string, error) {
for key, value := range SignatureTypeMap {
if value == expectedSignatureType {
return key, nil
}
}
return "", fmt.Errorf("unable to find signature type")
}
Loading
Loading