/ethereum-kms-sample

Demo app to sign ethereum tx with Google Cloud KMS

Primary LanguageTypeScriptApache License 2.0Apache-2.0

ethereum-kms-sample

Demo app to sign Ethereum tx with Google Cloud KMS.

Introduction

This repo is tutorial with example source code on how to use Google Cloud KMS to sign ethereum transactions with web3.js and Ethers.js.

Prerequisites

  1. Create a Google Cloud project, let's call it my-project.
  2. Install Google Cloud CLI and login to your Google Cloud account.
  3. Install and configure Git and Node.js.
  4. Clone this repo: git clone git@github.com:Hugoo/ethereum-kms-sample.git && cd ethereum-kms-sample
  5. Create .env file: cp .env.example .env
  6. Install dependencies: npm i

Tutorial

Create KMS keys

Let's start by creating our key in Google Cloud KMS. To do this, you can either use the gcloud CLI or the Google Cloud UI. We will use the gcloud CLI. Please note that you can also create these keys programmatically with the @google-cloud/kms library.

gcloud config set project my-project
> Updated property [core/project].

Create a key ring (reference):

gcloud kms keyrings create ethereum-keys --location=us-east1

You should see your new key ring in the Google Cloud Dashboard.

ethereum-keys key ring in google cloud

Then, use gcloud kms keys create to create a new key:

gcloud kms keys create key-1 --location=us-east1 --keyring=ethereum-keys --purpose=asymmetric-signing --default-algorithm=ec-sign-secp256k1-sha256 --protection-level=hsm

key-1 in google cloud

Purpose

This key will be used to sign our transactions, therefore we should set its purpose to asymmetric-signing. You can find more information about the purposes of a key here.

Algorithm

According to the Ethereum Yellow Paper Appendix F. Signing Transactions, transactions are signed using recoverable ECDSA signatures. The method uses the SECP-256k1 curve. Therefore, the algorithm of our key should be ec-sign-secp256k1-sha256.

The list of available algorithms can be found here.

Cool, now we have a key ready to be used to sign our transactions :)

Set up @google-cloud/kms

Now that our key is ready, we need to setup our Node.js project so it can use it. Depending on where your application will run, you may need to use a service account. If the application is running within Google Cloud, for instance, in Google App Engine, you can skip using a key and set the permissions directly to the App Engine default service account. For this tutorial, we will focus on running the scripts locally, therefore, we will need to handle the authentication of our script to Google Cloud through a service account.

Create a new service account:

gcloud iam service-accounts create ethereum-signer \
    --description="ethereum-signer" \
    --display-name="ethereum-signer"

> Created service account [ethereum-signer].

Grant the cloudkms.signerVerifier role to your service account:

gcloud projects add-iam-policy-binding my-project \
    --member serviceAccount:ethereum-signer@my-project.iam.gserviceaccount.com --role roles/cloudkms.signerVerifier
  • SA_ID: The ID of your service account. This can either be the service account's email address in the form SA_NAME@PROJECT_ID.iam.gserviceaccount.com, or the service account's unique numeric ID.

You can check if the service account was created correctly on the IAM Dashboard:

service account role

Finally, you can create a key file for this service account:

gcloud iam service-accounts keys create ./keys/signer-sa-key.json --iam-account=tx-signer@my-project.iam.gserviceaccount.com

You should have a key in ./keys/signer-sa-key.json.

Finally, let's install the @google-cloud/kms lib and verify if everything works

npm i @google-cloud/kms
npx ts-node src/check-keys.ts
{
  pem: '-----BEGIN PUBLIC KEY-----\n' +
    'MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAElBNf5/Pw2LHcKxP0DyhUaNRC8UpHOs0w\n' +
    'Ny+/0PSNdBoLrhLxZmeTVSwreMUOjabpM6TMjjBJgXLISOhW3SO0EA==\n' +
    '-----END PUBLIC KEY-----\n',
  algorithm: 'EC_SIGN_SECP256K1_SHA256',
  pemCrc32c: { value: '3112486914' },
  name: 'projects/my-project/locations/us-east1/keyRings/ethereum-keys/cryptoKeys/key-1/cryptoKeyVersions/1',
  protectionLevel: 'HSM'
}

From this pem string, we can compute our Ethereum address. For more information, you can check the ./src/check-keys.ts script and read the article about Playing with ethereum secp256k1 keys.

npx ts-node src/check-keys.ts

This script should output the expected Ethereum address associated with your KMS key.

Alchemy / Ropsten

We will need a RPC access to the Ethereum network, you can get one from Alchemy (this is an affiliate link).

alchemy create app

Then grab your API key:

alchemy app key

And add this key in your .env file for the ALCHEMY_API_KEY= variable.

You can also get rETH from a faucet, to fund your account (running the check-keys.ts script should give you your Ethereum address).

Ether.js

With Ethers.js, we can use the Signer API to connect to KMS.

Luckily, there is this super package which lets you use it very easily: https://github.com/openlawteam/ethers-gcp-kms-signer.

You can check the code in the ./src/ethersjs.ts file.

npx ts-node src/check-keys.ts
{
  type: 2,
  chainId: 3,
  nonce: 0,
  maxPriorityFeePerGas: BigNumber { _hex: '0x59682f00', _isBigNumber: true },
  maxFeePerGas: BigNumber { _hex: '0x11210f9eac', _isBigNumber: true },
  gasPrice: null,
  gasLimit: BigNumber { _hex: '0x5208', _isBigNumber: true },
  to: '0xE94E130546485b928C9C9b9A5e69EB787172952e',
  value: BigNumber { _hex: '0x038d7ea4c68000', _isBigNumber: true },
  data: '0x',
  accessList: [],
  hash: '0xda045876596e832703b131cfc5991a18617a0685dcdbd571da52273c6e481fbc',
  v: 0,
  r: '0x49c68f3df02aeb31d00b3326446973f0453c76cb7afc6ff8c78f7c00f928e8da',
  s: '0x522df0a097fc6452e75995c606b8de5793a7b9d1623d35753b113317fc6ca1fa',
  from: '0x19a7930683619396d06bdA6Ce43dc7A8659E7C20',
  confirmations: 0,
  wait: [Function (anonymous)]
}

web3.js - WIP

To use KMS with Web3js, we can either:

  • Create a provider
  • Create an account

The account should have this interface:

export interface Account {
  address: string;
  privateKey: string;
  signTransaction: (
    transactionConfig: TransactionConfig,
    callback?: (signTransaction: SignedTransaction) => void,
  ) => Promise<SignedTransaction>;
  sign: (data: string) => Sign;
  encrypt: (password: string) => EncryptedKeystoreV3Json;
}

References