Skip to content

Instantly share code, notes, and snippets.

@canhlinh
Last active August 9, 2023 03:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save canhlinh/08a6e317b62da8e4bdd5448e4f28eef0 to your computer and use it in GitHub Desktop.
Save canhlinh/08a6e317b62da8e4bdd5448e4f28eef0 to your computer and use it in GitHub Desktop.
sign and verify typed data golang
package main
import (
"crypto/ecdsa"
"crypto/subtle"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"log"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
"github.com/ethereum/go-ethereum/crypto"
)
const GELATO_RELAY_1BALANCE_ERC2771_ADDRESS = "0x7506C12a824d73D9b08564d5Afc22c949434755e"
var SponsoredCallERC2771PayloadToSign = apitypes.TypedData{
Types: apitypes.Types{
"SponsoredCallERC2771": []apitypes.Type{
{Name: "chainId", Type: "uint256"},
{Name: "target", Type: "address"},
{Name: "data", Type: "bytes"},
{Name: "user", Type: "address"},
{Name: "userNonce", Type: "uint256"},
{Name: "userDeadline", Type: "uint256"},
},
"EIP712Domain": []apitypes.Type{
{Name: "name", Type: "string"},
{Name: "chainId", Type: "uint256"},
{Name: "version", Type: "string"},
{Name: "verifyingContract", Type: "address"},
},
},
PrimaryType: "SponsoredCallERC2771",
Domain: apitypes.TypedDataDomain{
Name: "GelatoRelay1BalanceERC2771",
Version: "1",
ChainId: math.NewHexOrDecimal256(5),
VerifyingContract: GELATO_RELAY_1BALANCE_ERC2771_ADDRESS,
},
Message: apitypes.TypedDataMessage{
"chainId": math.NewHexOrDecimal256(5),
"target": "0x0d4241805D21F4Ca2348A525004181838d13828E",
"data": "0x30c7a3bb00000000000000000000000000000000000000000000000000000000000000030000000000000000000000001e4e1922a30d1488c8b5fbe88366fdcf100d102130783631373964346262623933343566313563643362653130366466336434640000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000008430783836613034313033646565663665313534343233663133663639373932323339386235616365626538383630646435316232373631653131656135303063626331343863316430373162646466323164383934373732326237346431343564363463656431326435336138326437393031633932356439383031623466333638316300000000000000000000000000000000000000000000000000000000",
"user": "0xEA8676243DF264f3A4e177703c13321D790D9bdA",
"userNonce": math.NewHexOrDecimal256(1000),
"userDeadline": math.NewHexOrDecimal256(1000),
},
}
func Sign(key *ecdsa.PrivateKey, data apitypes.TypedData) (hexutil.Bytes, error) {
dataHash, _, err := apitypes.TypedDataAndHash(data)
if err != nil {
return nil, err
}
signature, err := crypto.Sign(dataHash, key)
if err != nil {
return nil, err
}
if signature[crypto.RecoveryIDOffset] < 27 {
signature[crypto.RecoveryIDOffset] += 27
}
return signature, nil
}
type AuthToken struct {
TypedData string `json:"typedData"`
Signature string `json:"signature"`
Address string `json:"address"`
}
func struct2string(d any) string {
b, _ := json.Marshal(d)
return base64.URLEncoding.EncodeToString(b)
}
func main() {
privateKey, err := crypto.HexToECDSA("02527d7aeed90cfbddf6d08bca2484c2ea92c0e7e04e146dd5c0b3efe2f57c9a")
if err != nil {
log.Fatal(err)
}
signature, err := Sign(privateKey, SponsoredCallERC2771PayloadToSign)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Signature: %s\n", signature.String())
fmt.Println("Addresss:", crypto.PubkeyToAddress(privateKey.PublicKey).Hex())
d, err := json.Marshal(&AuthToken{
Address: crypto.PubkeyToAddress(privateKey.PublicKey).Hex(),
Signature: signature.String(),
TypedData: struct2string(SponsoredCallERC2771PayloadToSign),
})
if err != nil {
log.Fatal(err)
}
s, err := verifyAuthTokenAddress(string(d))
if err != nil {
log.Fatal("failed to verify")
}
fmt.Println("VerifedAddress:", s)
}
func verifyAuthTokenAddress(data string) (string, error) {
var authToken AuthToken
if err := json.Unmarshal([]byte(data), &authToken); err != nil {
return "", fmt.Errorf("unmarshal auth token: %w", err)
}
signature, err := hexutil.Decode(authToken.Signature)
if err != nil {
return "", fmt.Errorf("decode signature: %w", err)
}
// fmt.Println("SIG:", hexutil.Encode(signature))
typedDataBytes, err := base64.StdEncoding.DecodeString(authToken.TypedData)
if err != nil {
return "", fmt.Errorf("decode typed data 1: %w", err)
}
typedData := apitypes.TypedData{}
if err := json.Unmarshal(typedDataBytes, &typedData); err != nil {
return "", fmt.Errorf("unmarshal typed data 2: %w", err)
}
// EIP-712 typed data marshalling
domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map())
if err != nil {
return "", fmt.Errorf("eip712domain hash struct: %w", err)
}
typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message)
if err != nil {
return "", fmt.Errorf("primary type hash struct: %w", err)
}
// add magic string prefix
rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash)))
sighash := crypto.Keccak256(rawData)
// fmt.Println("SIG HASH:", hexutil.Encode(sighash))
// update the recovery id
// https://github.com/ethereum/go-ethereum/blob/55599ee95d4151a2502465e0afc7c47bd1acba77/internal/ethapi/api.go#L442
signature[64] -= 27
// get the pubkey used to sign this signature
sigPubkey, err := crypto.Ecrecover(sighash, signature)
if err != nil {
return "", fmt.Errorf("ecrecover: %w", err)
}
// fmt.Println("SIG PUBKEY:", hexutil.Encode(sigPubkey))
// get the address to confirm it's the same one in the auth token
pubkey, err := crypto.UnmarshalPubkey(sigPubkey)
if err != nil {
return "", err
}
address := crypto.PubkeyToAddress(*pubkey)
// fmt.Println("ADDRESS:", address.Hex())
// verify the signature (not sure if this is actually required after ecrecover)
signatureNoRecoverID := signature[:len(signature)-1]
verified := crypto.VerifySignature(sigPubkey, sighash, signatureNoRecoverID)
if !verified {
return "", errors.New("verification failed")
}
// fmt.Println("VERIFIED:", verified)
tokenAddress := common.HexToAddress(authToken.Address)
if subtle.ConstantTimeCompare(address.Bytes(), tokenAddress.Bytes()) == 0 {
return "", errors.New("address mismatch")
}
return address.Hex(), nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment