Last active
August 9, 2023 03:28
-
-
Save canhlinh/08a6e317b62da8e4bdd5448e4f28eef0 to your computer and use it in GitHub Desktop.
sign and verify typed data golang
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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